Skip to content

Commit

Permalink
Fixes #2146 by implementing IngressClass for v1beta1.Ingress objects
Browse files Browse the repository at this point in the history
Add support for IngressClass objects which match the controller name of 'projectcontour.io/ingress-controller'.
Any Ingress object which defines an 'Spec.IngressClassName', Contour will validate that a corresponding IngressClass
exists. Current ingress class logic for Contour is not affected and still take priority over any IngressClass logic.

Signed-off-by: Steve Sloka <[email protected]>
  • Loading branch information
stevesloka committed Jun 26, 2020
1 parent bb4f6dc commit 4d8c977
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 48 deletions.
2 changes: 2 additions & 0 deletions cmd/contour/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ func doServe(log logrus.FieldLogger, ctx *serveContext) error {
dynamicHandler, k8s.ServiceAPIResources()...)
}

informerSyncList.InformOnResources(clusterInformerFactory, dynamicHandler, k8s.IngressClassResources(clients.ServerVersion)...)

// TODO(youngnick): Move this logic out to internal/k8s/informers.go somehow.
// Add informers for each root namespace
for _, factory := range namespacedInformerFactories {
Expand Down
22 changes: 10 additions & 12 deletions internal/annotation/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,25 +191,23 @@ func PerTryTimeout(i *v1beta1.Ingress) time.Duration {
// 1. projectcontour.io/ingress.class
// 2. contour.heptio.com/ingress.class
// 3. kubernetes.io/ingress.class
func IngressClass(o metav1.ObjectMetaAccessor) string {
a := o.GetObjectMeta().GetAnnotations()
if class, ok := a["projectcontour.io/ingress.class"]; ok {
return class
func IngressClass(annotations map[string]string) (bool, string) {
if class, ok := annotations["projectcontour.io/ingress.class"]; ok {
return true, class
}
if class, ok := a["contour.heptio.com/ingress.class"]; ok {
return class
if class, ok := annotations["contour.heptio.com/ingress.class"]; ok {
return true, class
}
if class, ok := a["kubernetes.io/ingress.class"]; ok {
return class
if class, ok := annotations["kubernetes.io/ingress.class"]; ok {
return true, class
}
return ""
return false, ""
}

// MatchesIngressClass checks that the passed object has an ingress class that matches
// either the passed ingress-class string, or DEFAULT_INGRESS_CLASS if it's empty.
func MatchesIngressClass(o metav1.ObjectMetaAccessor, ic string) bool {

switch IngressClass(o) {
func MatchesIngressClass(objectClass, ic string) bool {
switch objectClass {
case ic:
// Handles ic == "" and ic == "custom".
return true
Expand Down
3 changes: 2 additions & 1 deletion internal/annotation/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,8 @@ func TestMatchIngressClass(t *testing.T) {
t.Run(name, func(t *testing.T) {
cases := []string{"", DEFAULT_INGRESS_CLASS}
for i := 0; i < len(cases); i++ {
got := MatchesIngressClass(tc.fixture, cases[i])
_, ic := IngressClass(tc.fixture.GetObjectMeta().GetAnnotations())
got := MatchesIngressClass(ic, cases[i])
if tc.want[i] != got {
t.Errorf("matching %v against ingress class %q: expected %v, got %v", tc.fixture, cases[i], tc.want[i], got)
}
Expand Down
4 changes: 2 additions & 2 deletions internal/dag/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func (b *Builder) validHTTPProxies() []*projcontour.HTTPProxy {
// computeSecureVirtualhosts populates tls parameters of
// secure virtual hosts.
func (b *Builder) computeSecureVirtualhosts() {
for _, ing := range b.Source.ingresses {
for _, ing := range b.Source.Ingresses() {
for _, tls := range ing.Spec.TLS {
secretName := splitSecret(tls.SecretName, ing.GetNamespace())

Expand Down Expand Up @@ -298,7 +298,7 @@ func (b *Builder) delegationPermitted(secret k8s.FullName, to string) bool {

func (b *Builder) computeIngresses() {
// deconstruct each ingress into routes and virtualhost entries
for _, ing := range b.Source.ingresses {
for _, ing := range b.Source.Ingresses() {

// rewrite the default ingress to a stock ingress rule.
rules := rulesFromSpec(ing.Spec)
Expand Down
64 changes: 52 additions & 12 deletions internal/dag/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package dag

import (
"fmt"
"sync"

"github.com/projectcontour/contour/internal/annotation"
Expand All @@ -40,6 +41,7 @@ type KubernetesCache struct {
IngressClass string

ingresses map[k8s.FullName]*v1beta1.Ingress
ingressclasses map[string]*v1beta1.IngressClass
httpproxies map[k8s.FullName]*projectcontour.HTTPProxy
secrets map[k8s.FullName]*v1.Secret
httpproxydelegations map[k8s.FullName]*projectcontour.TLSCertificateDelegation
Expand All @@ -57,6 +59,7 @@ type KubernetesCache struct {
// init creates the internal cache storage. It is called implicitly from the public API.
func (kc *KubernetesCache) init() {
kc.ingresses = make(map[k8s.FullName]*v1beta1.Ingress)
kc.ingressclasses = make(map[string]*v1beta1.IngressClass)
kc.httpproxies = make(map[k8s.FullName]*projectcontour.HTTPProxy)
kc.secrets = make(map[k8s.FullName]*v1.Secret)
kc.httpproxydelegations = make(map[k8s.FullName]*projectcontour.TLSCertificateDelegation)
Expand All @@ -67,19 +70,33 @@ func (kc *KubernetesCache) init() {
kc.tcproutes = make(map[k8s.FullName]*serviceapis.TcpRoute)
}

func (kc *KubernetesCache) Ingresses() []*v1beta1.Ingress {
var ings []*v1beta1.Ingress
for _, ing := range kc.ingresses {
if kc.matchesIngressClass(ing.Annotations, ing.Spec.IngressClassName) {
ings = append(ings, ing)
}
}
return ings
}

// matchesIngressClass returns true if the given Kubernetes object
// belongs to the Ingress class that this cache is using.
func (kc *KubernetesCache) matchesIngressClass(obj k8s.Object) bool {
func (kc *KubernetesCache) matchesIngressClass(annotations map[string]string, ingressClassName *string) bool {

if !annotation.MatchesIngressClass(obj, kc.IngressClass) {
kind := k8s.KindOf(obj)
om := obj.GetObjectMeta()
// check if the object has an annotation
if found, classAnnotation := annotation.IngressClass(annotations); found {
return annotation.MatchesIngressClass(classAnnotation, kc.IngressClass)
}

kc.WithField("name", om.GetName()).
WithField("namespace", om.GetNamespace()).
WithField("kind", kind).
WithField("ingress.class", annotation.IngressClass(obj)).
Debug("ignoring object with unmatched ingress class")
if ingressClassName != nil {
// check if this class matches a existing IngressClass
_, ok := kc.ingressclasses[*ingressClassName]
return ok
}

// if no annotations are supplied or ingressClassNames, then verify if ingressClass is configured
if kc.IngressClass != annotation.DEFAULT_INGRESS_CLASS && kc.IngressClass != "" {
return false
}

Expand Down Expand Up @@ -136,13 +153,32 @@ func (kc *KubernetesCache) Insert(obj interface{}) bool {
case *v1.Service:
kc.services[k8s.ToFullName(obj)] = obj
return kc.serviceTriggersRebuild(obj)
case *v1beta1.IngressClass:
// Match only ingress classes that Contour should be able to own.
if obj.Spec.Controller == "projectcontour.io/ingress-controller" {
kc.ingressclasses[obj.Name] = obj
fmt.Println("---- added ingressclass!")
return true
}
case *v1beta1.Ingress:
if kc.matchesIngressClass(obj) {
kc.ingresses[k8s.ToFullName(obj)] = obj
matches := kc.matchesIngressClass(obj.Annotations, obj.Spec.IngressClassName)
kc.ingresses[k8s.ToFullName(obj)] = obj

if matches {
return true
}

kind := k8s.KindOf(obj)
om := obj.GetObjectMeta()

_, ic := annotation.IngressClass(obj.Annotations)
kc.WithField("name", om.GetName()).
WithField("namespace", om.GetNamespace()).
WithField("kind", kind).
WithField("ingress.class", ic).
Debug("ignoring object with unmatched ingress class")
case *projectcontour.HTTPProxy:
if kc.matchesIngressClass(obj) {
if kc.matchesIngressClass(obj.Annotations, nil) {
kc.httpproxies[k8s.ToFullName(obj)] = obj
return true
}
Expand Down Expand Up @@ -212,6 +248,10 @@ func (kc *KubernetesCache) remove(obj interface{}) bool {
_, ok := kc.services[m]
delete(kc.services, m)
return ok
case *v1beta1.IngressClass:
_, ok := kc.ingressclasses[obj.Name]
delete(kc.ingressclasses, obj.Name)
return ok
case *v1beta1.Ingress:
m := k8s.ToFullName(obj)
_, ok := kc.ingresses[m]
Expand Down
152 changes: 134 additions & 18 deletions internal/dag/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ import (
v1 "k8s.io/api/core/v1"
"k8s.io/api/networking/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utils "k8s.io/utils/pointer"
serviceapis "sigs.k8s.io/service-apis/api/v1alpha1"
)

func TestKubernetesCacheInsert(t *testing.T) {
tests := map[string]struct {
pre []interface{}
obj interface{}
want bool
pre []interface{}
obj interface{}
ingressClass string
want bool
}{
"insert secret": {
obj: &v1.Secret{
Expand Down Expand Up @@ -403,6 +405,28 @@ func TestKubernetesCacheInsert(t *testing.T) {
},
want: true,
},
"insert ingress class not controlled by contour": {
obj: &v1beta1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: "notcontour",
},
Spec: v1beta1.IngressClassSpec{
Controller: "anothercontroller/ingress-controller",
},
},
want: false,
},
"insert valid ingress class": {
obj: &v1beta1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: "contour",
},
Spec: v1beta1.IngressClassSpec{
Controller: "projectcontour.io/ingress-controller",
},
},
want: true,
},
"insert ingress empty ingress class": {
obj: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -412,6 +436,99 @@ func TestKubernetesCacheInsert(t *testing.T) {
},
want: true,
},
"insert valid ingress class name": {
pre: []interface{}{
&v1beta1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
},
Spec: v1beta1.IngressClassSpec{
Controller: "projectcontour.io/ingress-controller",
},
},
},
obj: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "incorrect",
Namespace: "default",
},
Spec: v1beta1.IngressSpec{
IngressClassName: utils.StringPtr("simple"),
},
},
want: true,
},
"insert valid ingress class name matching annotation": {
pre: []interface{}{
&v1beta1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
},
Spec: v1beta1.IngressClassSpec{
Controller: "projectcontour.io/ingress-controller",
},
},
},
obj: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "incorrect",
Namespace: "default",
Annotations: map[string]string{
"projectcontour.io/ingress.class": "custom",
},
},
Spec: v1beta1.IngressSpec{
IngressClassName: utils.StringPtr("simple"),
},
},
ingressClass: "custom",
want: true,
},
"insert non-matching ingress class name": {
pre: []interface{}{
&v1beta1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
},
Spec: v1beta1.IngressClassSpec{
Controller: "projectcontour.io/ingress-controller",
},
},
},
obj: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "incorrect",
Namespace: "default",
},
Spec: v1beta1.IngressSpec{
IngressClassName: utils.StringPtr("custom"),
},
},
want: false,
},
"insert matching ingress class name, but with different ingressClass flag specified": {
pre: []interface{}{
&v1beta1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: "simple",
},
Spec: v1beta1.IngressClassSpec{
Controller: "projectcontour.io/ingress-controller",
},
},
},
obj: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "incorrect",
Namespace: "default",
},
Spec: v1beta1.IngressSpec{
IngressClassName: utils.StringPtr("simple"),
},
},
ingressClass: "custom",
want: true,
},
"insert ingress incorrect kubernetes.io/ingress.class": {
obj: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -729,7 +846,8 @@ func TestKubernetesCacheInsert(t *testing.T) {
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
cache := KubernetesCache{
FieldLogger: testLogger(t),
IngressClass: tc.ingressClass,
FieldLogger: testLogger(t),
}
for _, p := range tc.pre {
cache.Insert(p)
Expand Down Expand Up @@ -809,26 +927,24 @@ func TestKubernetesCacheRemove(t *testing.T) {
},
want: true,
},
"remove ingress incorrect ingressclass": {
cache: cache(&v1beta1.Ingress{
"remove ingressclass": {
cache: cache(&v1beta1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress",
Namespace: "default",
Annotations: map[string]string{
"kubernetes.io/ingress.class": "nginx",
},
Name: "ingressclass",
},
Spec: v1beta1.IngressClassSpec{
Controller: "projectcontour.io/ingress-controller",
},
}),
obj: &v1beta1.Ingress{
obj: &v1beta1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress",
Namespace: "default",
Annotations: map[string]string{
"kubernetes.io/ingress.class": "nginx",
},
Name: "ingressclass",
},
Spec: v1beta1.IngressClassSpec{
Controller: "projectcontour.io/ingress-controller",
},
},
want: false,
want: true,
},
"remove httpproxy": {
cache: cache(&projcontour.HTTPProxy{
Expand Down
Loading

0 comments on commit 4d8c977

Please sign in to comment.