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 Jul 15, 2020
1 parent 55b3a89 commit d98dc6c
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 53 deletions.
7 changes: 7 additions & 0 deletions cmd/contour/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,13 @@ func doServe(log logrus.FieldLogger, ctx *serveContext) error {
}
}

// Check if the resource exists in the API server before setting up the informer.
if !clients.ResourceExists(k8s.IngressClassResources()...) {
log.WithField("InformOnResources", "Ingressclass Resources").Warnf("resources %v not found in api server", k8s.IngressClassResources())
} else {
informerSyncList.InformOnResources(clusterInformerFactory, dynamicHandler, k8s.IngressClassResources()...)
}

// TODO(youngnick): Move this logic out to internal/k8s/informers.go somehow.
// Add informers for each root namespace
for _, factory := range namespacedInformerFactories {
Expand Down
8 changes: 8 additions & 0 deletions examples/contour/02-role-contour.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ rules:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingressclasses
verbs:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
Expand Down
8 changes: 8 additions & 0 deletions examples/render/contour.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,14 @@ rules:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
- ingressclasses
verbs:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
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 @@ -230,7 +230,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 @@ -299,7 +299,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
Loading

0 comments on commit d98dc6c

Please sign in to comment.