Skip to content

Commit

Permalink
Fixes projectcontour#2146 by implementing IngressClass for v1beta1.In…
Browse files Browse the repository at this point in the history
…gress objects

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 30, 2020
1 parent 386c2d8 commit 6d1d7bb
Show file tree
Hide file tree
Showing 11 changed files with 406 additions and 50 deletions.
2 changes: 2 additions & 0 deletions cmd/contour/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,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
25 changes: 12 additions & 13 deletions internal/annotation/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,29 +187,28 @@ func PerTryTimeout(i *v1beta1.Ingress) time.Duration {
}

// IngressClass returns the first matching ingress class for the following
// annotations:
// annotations as well as returning a boolean value if an annotation was found
// and not just hitting the default case:
// 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
74 changes: 61 additions & 13 deletions internal/dag/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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 +58,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,24 +69,40 @@ func (kc *KubernetesCache) init() {
kc.tcproutes = make(map[k8s.FullName]*serviceapis.TcpRoute)
}

// Ingresses returns a set of valid Ingress objects that match proper
// ingress class logic via annotations or IngressClasses.
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 and that the annotation was found.
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
}

// No annotations found, ingress classes, or configured ingressclass so object is then valid.
return true

}

// Insert inserts obj into the KubernetesCache.
Expand Down Expand Up @@ -136,13 +154,39 @@ 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
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 := func() string {
if found, a := annotation.IngressClass(obj.Annotations); found {
return a
}
if obj.Spec.IngressClassName != nil {
return *obj.Spec.IngressClassName
}
return ""
}
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 +256,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 6d1d7bb

Please sign in to comment.