Skip to content

Commit 8967b95

Browse files
ivanmatmatioktalz
authored andcommitted
MEDIUM: merge ingresses annotations pointing to same backend
1 parent 01b121f commit 8967b95

File tree

7 files changed

+227
-20
lines changed

7 files changed

+227
-20
lines changed

pkg/annotations/annotations.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,38 @@ func Timeout(name string, annotations ...map[string]string) (out *int64, err err
244244
}
245245
return
246246
}
247+
248+
// SpecificAnnotations is a set of annotations that uses rules to produce specific configuration with rule ID in configuration file.
249+
// These annotations in an ingress can't be merged with other ingresses annotations when these ingresses point to the same service because specific paths must be treated specifically.
250+
var SpecificAnnotations = map[string]struct{}{
251+
"backend-config-snippet": {},
252+
"deny-list": {},
253+
"blacklist": {},
254+
"allow-list": {},
255+
"whitelist": {},
256+
"src-ip-header": {},
257+
"auth-type": {},
258+
"auth-realm": {},
259+
"auth-secret": {},
260+
"ssl-redirect": {},
261+
"ssl-redirect-port": {},
262+
"ssl-redirect-code": {},
263+
"request-redirect": {},
264+
"request-redirect-code": {},
265+
"request-capture": {},
266+
"request-capture-len": {},
267+
"path-rewrite": {},
268+
"rate-limit-requests": {},
269+
"rate-limit-period": {},
270+
"rate-limit-size": {},
271+
"rate-limit-status-code": {},
272+
"request-set-header": {},
273+
"response-set-header": {},
274+
"set-host": {},
275+
"cors-enable": {},
276+
"cors-allow-origin": {},
277+
"cors-allow-methods": {},
278+
"cors-allow-headers": {},
279+
"cors-max-age": {},
280+
"cors-allow-credentials": {},
281+
}

pkg/controller/controller.go

Lines changed: 104 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121

2222
"github.com/go-test/deep"
2323

24+
maps0 "maps"
25+
2426
"github.com/haproxytech/client-native/v6/models"
2527
"github.com/haproxytech/kubernetes-ingress/pkg/annotations"
2628
"github.com/haproxytech/kubernetes-ingress/pkg/fs"
@@ -150,26 +152,7 @@ func (c *HAProxyController) updateHAProxy() {
150152
logger.Error(err)
151153
}
152154

153-
for _, namespace := range c.store.Namespaces {
154-
c.store.SecretsProcessed = map[string]struct{}{}
155-
for _, ingResource := range namespace.Ingresses {
156-
if !namespace.Relevant && !ingResource.Faked {
157-
// As we watch only for white-listed namespaces, we should not worry about iterating over
158-
// many ingresses in irrelevant namespaces.
159-
// There should only be fake ingresses in irrelevant namespaces so loop should be whithin small amount of ingresses (Prometheus)
160-
continue
161-
}
162-
i := ingress.New(ingResource, c.osArgs.IngressClass, c.osArgs.EmptyIngressClass, c.annotations)
163-
if !i.Supported(c.store, c.annotations) {
164-
logger.Debugf("ingress '%s/%s' ignored: no matching", ingResource.Namespace, ingResource.Name)
165-
} else {
166-
i.Update(c.store, c.haproxy, c.annotations)
167-
}
168-
if ingResource.Status == store.ADDED || ingResource.ClassUpdated {
169-
c.updateStatusManager.AddIngress(i)
170-
}
171-
}
172-
}
155+
c.processIngressesWithMerge()
173156

174157
updated := deep.Equal(route.CurentCustomRoutes, route.CustomRoutes, deep.FLAG_IGNORE_SLICE_ORDER)
175158
if len(updated) != 0 {
@@ -339,3 +322,104 @@ func (c *HAProxyController) clean(failedSync bool) {
339322
func (c *HAProxyController) SetGatewayAPIInstalled(gatewayAPIInstalled bool) {
340323
c.gatewayManager.SetGatewayAPIInstalled(gatewayAPIInstalled)
341324
}
325+
326+
func (c *HAProxyController) manageIngress(ing *store.Ingress) {
327+
i := ingress.New(ing, c.osArgs.IngressClass, c.osArgs.EmptyIngressClass, c.annotations)
328+
if !i.Supported(c.store, c.annotations) {
329+
logger.Debugf("ingress '%s/%s' ignored: no matching", ing.Namespace, ing.Name)
330+
} else {
331+
i.Update(c.store, c.haproxy, c.annotations)
332+
}
333+
if ing.Status == store.ADDED || ing.ClassUpdated {
334+
c.updateStatusManager.AddIngress(i)
335+
}
336+
}
337+
338+
func (c *HAProxyController) processIngressesWithMerge() {
339+
for _, namespace := range c.store.Namespaces {
340+
c.store.SecretsProcessed = map[string]struct{}{}
341+
// Iterate over services
342+
for _, service := range namespace.Services {
343+
ingressesOrderedList := c.store.IngressesByService[service.Namespace+"/"+service.Name]
344+
if ingressesOrderedList == nil {
345+
continue
346+
}
347+
ingresses := ingressesOrderedList.Items()
348+
if len(ingresses) == 0 {
349+
continue
350+
}
351+
// Put standalone ingresses aside.
352+
var standaloneIngresses []*store.Ingress
353+
// Get the name of ingresses referring to the service
354+
var ingressesToMerge []*store.Ingress
355+
for _, ing := range ingresses {
356+
i := ingress.New(ing, c.osArgs.IngressClass, c.osArgs.EmptyIngressClass, c.annotations)
357+
if !i.Supported(c.store, c.annotations) {
358+
continue
359+
}
360+
// if the ingress has standalone-backend annotation, put it aside and continue.
361+
if ing.Annotations["standalone-backend"] == "true" {
362+
standaloneIngresses = append(standaloneIngresses, ing)
363+
continue
364+
}
365+
ingressesToMerge = append(ingressesToMerge, ing)
366+
}
367+
368+
// Get copy of annotationsFromAllIngresses from all ingresses
369+
annotationsFromAllIngresses := map[string]string{}
370+
371+
for _, ingressToMerge := range ingressesToMerge {
372+
// Gather all annotations from all ingresses referring to the service in a consistent order based on ingress name.
373+
for ann, value := range ingressToMerge.Annotations {
374+
if _, specific := annotations.SpecificAnnotations[ann]; specific {
375+
continue
376+
}
377+
annotationsFromAllIngresses[ann] = value
378+
}
379+
}
380+
381+
// Now we've gathered the annotations set we can process all ingresses.
382+
for _, ingressToMerge := range ingressesToMerge {
383+
// We copy the ingress
384+
consolidatedIngress := *ingressToMerge
385+
// We assign the general set of annotations
386+
consolidatedIngressAnns := map[string]string{}
387+
maps0.Copy(consolidatedIngressAnns, annotationsFromAllIngresses)
388+
389+
consolidatedIngress.Annotations = consolidatedIngressAnns
390+
for ann, value := range ingressToMerge.Annotations {
391+
if _, specific := annotations.SpecificAnnotations[ann]; !specific {
392+
continue
393+
}
394+
consolidatedIngress.Annotations[ann] = value
395+
}
396+
// We will reprocess the rules because we need to skip the ones referring to an other service.
397+
rules := map[string]*store.IngressRule{}
398+
consolidatedIngress.Rules = rules
399+
for _, rule := range ingressToMerge.Rules {
400+
newRule := store.IngressRule{
401+
Host: rule.Host,
402+
Paths: map[string]*store.IngressPath{},
403+
}
404+
for _, path := range rule.Paths {
405+
// if the rule refers to the service then keep it ...
406+
if path.SvcNamespace == service.Namespace && path.SvcName == service.Name {
407+
newRule.Paths[path.Path] = path
408+
}
409+
}
410+
// .. if it's not empty
411+
if len(newRule.Paths) > 0 {
412+
rules[newRule.Host] = &newRule
413+
}
414+
}
415+
// Back to the usual processing of the ingress
416+
417+
c.manageIngress(&consolidatedIngress)
418+
}
419+
// Now process the standalone ingresses as usual.
420+
for _, standaloneIngress := range standaloneIngresses {
421+
c.manageIngress(standaloneIngress)
422+
}
423+
}
424+
}
425+
}

pkg/store/convert.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ func (n ingressNetworkingV1Strategy) ConvertIngress() *Ingress {
158158
}
159159
return tls
160160
}(n.ig.Spec.TLS),
161+
CreationTime: n.ig.CreationTimestamp.Time,
161162
},
162163
}
163164
addresses := []string{}

pkg/store/events.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ func (k *K8s) EventIngress(ns *Namespace, data *Ingress, uid types.UID, resource
6464

6565
if data.Status == DELETED {
6666
delete(ns.Ingresses, data.Name)
67+
for _, rule := range data.Rules {
68+
for _, path := range rule.Paths {
69+
k.IngressesByService[path.SvcNamespace+"/"+path.SvcName].Remove(data)
70+
}
71+
}
6772
meta.GetMetaStore().ProcessedResourceVersion.Delete(data, uid)
6873
} else {
6974
if oldIngress, ok := ns.Ingresses[data.Name]; ok {
@@ -81,9 +86,36 @@ func (k *K8s) EventIngress(ns *Namespace, data *Ingress, uid types.UID, resource
8186
if data.Annotations["ingress.class"] != oldIngress.Annotations["ingress.class"] {
8287
data.ClassUpdated = true
8388
}
89+
90+
for _, rule := range oldIngress.Rules {
91+
for _, path := range rule.Paths {
92+
k.IngressesByService[path.SvcNamespace+"/"+path.SvcName].Remove(data)
93+
}
94+
}
8495
}
8596
ns.Ingresses[data.Name] = data
8697
meta.GetMetaStore().ProcessedResourceVersion.Set(data, uid, resourceVersion)
98+
99+
for _, rule := range data.Rules {
100+
for _, path := range rule.Paths {
101+
key := path.SvcNamespace + "/" + path.SvcName
102+
ingresses := k.IngressesByService[key]
103+
104+
if ingresses == nil {
105+
ingresses = utils.NewOrderedSet[string, *Ingress](func(i *Ingress) string { return i.Name },
106+
func(a, b *Ingress) bool {
107+
// We need to consider the case where two ingresses are created in the same second.
108+
// Otherwise there could be any order in two instances;
109+
if a.CreationTime.Equal(b.CreationTime) {
110+
return a.Namespace+a.Name < b.Namespace+b.Name
111+
}
112+
return a.CreationTime.After(b.CreationTime)
113+
})
114+
k.IngressesByService[key] = ingresses
115+
}
116+
ingresses.Add(data)
117+
}
118+
}
87119
}
88120
return
89121
}

pkg/store/store.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type K8s struct {
4343
GatewayControllerName string
4444
PublishServiceAddresses []string
4545
UpdateAllIngresses bool
46+
IngressesByService map[string]*utils.OrderedSet[string, *Ingress] // service fqn -> ingress name -> ingress
4647
}
4748

4849
type NamespacesWatch struct {
@@ -86,6 +87,7 @@ func NewK8sStore(args utils.OSArgs) K8s {
8687
BackendsWithNoConfigSnippets: map[string]struct{}{},
8788
HaProxyPods: map[string]struct{}{},
8889
FrontendRC: rc.NewResourceCounter(),
90+
IngressesByService: map[string]*utils.OrderedSet[string, *Ingress]{},
8991
}
9092
for _, namespace := range args.NamespaceWhitelist {
9193
store.NamespacesAccess.Whitelist[namespace] = struct{}{}

pkg/store/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ type IngressCore struct {
186186
Namespace string
187187
Name string
188188
Class string
189+
CreationTime time.Time
189190
}
190191

191192
type GatewayClass struct {

pkg/utils/orderedset.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package utils
2+
3+
import "sort"
4+
5+
type OrderedSet[K comparable, T any] struct {
6+
items []T
7+
seen map[K]struct{}
8+
keyFn func(T) K
9+
lessFn func(a, b T) bool
10+
}
11+
12+
func NewOrderedSet[K comparable, T any](keyFn func(T) K, lessFn func(a, b T) bool) *OrderedSet[K, T] {
13+
return &OrderedSet[K, T]{
14+
items: []T{},
15+
seen: make(map[K]struct{}),
16+
keyFn: keyFn,
17+
lessFn: lessFn,
18+
}
19+
}
20+
21+
func (os *OrderedSet[K, T]) Add(item T) {
22+
key := os.keyFn(item)
23+
if _, exists := os.seen[key]; exists {
24+
return
25+
}
26+
27+
i := sort.Search(len(os.items), func(i int) bool {
28+
return os.lessFn(item, os.items[i])
29+
})
30+
os.items = append(os.items, item)
31+
copy(os.items[i+1:], os.items[i:])
32+
os.items[i] = item
33+
os.seen[key] = struct{}{}
34+
}
35+
36+
func (os *OrderedSet[K, T]) Remove(item T) {
37+
key := os.keyFn(item)
38+
if _, exists := os.seen[key]; !exists {
39+
return
40+
}
41+
delete(os.seen, key)
42+
for i, v := range os.items {
43+
if os.keyFn(v) == key {
44+
os.items = append(os.items[:i], os.items[i+1:]...)
45+
break
46+
}
47+
}
48+
}
49+
50+
func (os *OrderedSet[K, T]) Items() []T {
51+
return os.items
52+
}

0 commit comments

Comments
 (0)