Skip to content

Commit 199b19a

Browse files
committed
interceptor: support for http header routing
Signed-off-by: Jan Wozniak <wozniak.jan@gmail.com>
1 parent d891e6e commit 199b19a

File tree

10 files changed

+679
-113
lines changed

10 files changed

+679
-113
lines changed

config/crd/bases/http.keda.sh_httpscaledobjects.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,27 @@ spec:
6060
spec:
6161
description: HTTPScaledObjectSpec defines the desired state of HTTPScaledObject
6262
properties:
63+
headers:
64+
description: |-
65+
The custom headers used to route. Once Hosts and PathPrefixes have been matched,
66+
if at least one header in the http request matches at least one header
67+
in .spec.headers, it will be routed to the Service and Port specified in
68+
the scaleTargetRef. First header it matches with, it will be routed to.
69+
If the headers can't be matched, then use first one without .spec.headers supplied
70+
If that doesn't exist then routing will fail.
71+
items:
72+
description: Header contains the definition for matching on header
73+
name and/or value
74+
properties:
75+
name:
76+
type: string
77+
value:
78+
type: string
79+
required:
80+
- name
81+
- value
82+
type: object
83+
type: array
6384
hosts:
6485
description: |-
6586
The hosts to route. All requests which the "Host" header

operator/apis/http/v1alpha1/httpscaledobject_types.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ type RateMetricSpec struct {
7676
Granularity metav1.Duration `json:"granularity" description:"Time granularity for rate calculation"`
7777
}
7878

79+
// Header contains the definition for matching on header name and/or value
80+
type Header struct {
81+
Name string `json:"name"`
82+
Value string `json:"value"`
83+
}
84+
7985
// HTTPScaledObjectSpec defines the desired state of HTTPScaledObject
8086
type HTTPScaledObjectSpec struct {
8187
// The hosts to route. All requests which the "Host" header
@@ -89,6 +95,14 @@ type HTTPScaledObjectSpec struct {
8995
// the scaleTargetRef.
9096
// +optional
9197
PathPrefixes []string `json:"pathPrefixes,omitempty"`
98+
// The custom headers used to route. Once Hosts and PathPrefixes have been matched,
99+
// if at least one header in the http request matches at least one header
100+
// in .spec.headers, it will be routed to the Service and Port specified in
101+
// the scaleTargetRef. First header it matches with, it will be routed to.
102+
// If the headers can't be matched, then use first one without .spec.headers supplied
103+
// If that doesn't exist then routing will fail.
104+
// +optional
105+
Headers []Header `json:"headers,omitempty"`
92106
// The name of the deployment to route HTTP requests to (and to autoscale).
93107
// Including validation as a requirement to define either the PortName or the Port
94108
// +kubebuilder:validation:XValidation:rule="has(self.portName) != has(self.port)",message="must define either the 'portName' or the 'port'"

operator/apis/http/v1alpha1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/routing/httpso_index.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package routing
2+
3+
import (
4+
iradix "github.com/hashicorp/go-immutable-radix/v2"
5+
6+
httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1"
7+
)
8+
9+
type httpSOIndex struct {
10+
radix *iradix.Tree[*httpv1alpha1.HTTPScaledObject]
11+
}
12+
13+
func newHTTPSOIndex() *httpSOIndex {
14+
return &httpSOIndex{radix: iradix.New[*httpv1alpha1.HTTPScaledObject]()}
15+
}
16+
17+
func (hi *httpSOIndex) insert(key tableMemoryIndexKey, httpso *httpv1alpha1.HTTPScaledObject) (*httpSOIndex, *httpv1alpha1.HTTPScaledObject, bool) {
18+
newRadix, oldVal, oldSet := hi.radix.Insert(key, httpso)
19+
newHTTPSOIndex := &httpSOIndex{
20+
radix: newRadix,
21+
}
22+
return newHTTPSOIndex, oldVal, oldSet
23+
}
24+
25+
func (hi *httpSOIndex) get(key tableMemoryIndexKey) (*httpv1alpha1.HTTPScaledObject, bool) {
26+
return hi.radix.Get(key)
27+
}
28+
29+
func (hi *httpSOIndex) delete(key tableMemoryIndexKey) (*httpSOIndex, *httpv1alpha1.HTTPScaledObject, bool) {
30+
newRadix, oldVal, oldSet := hi.radix.Delete(key)
31+
newHTTPSOIndex := &httpSOIndex{
32+
radix: newRadix,
33+
}
34+
return newHTTPSOIndex, oldVal, oldSet
35+
}

pkg/routing/httpso_index_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package routing
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
8+
httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1"
9+
"github.com/kedacore/http-add-on/pkg/k8s"
10+
)
11+
12+
var _ = Describe("httpSOIndex", func() {
13+
var (
14+
httpso0 = &httpv1alpha1.HTTPScaledObject{
15+
ObjectMeta: metav1.ObjectMeta{
16+
Name: "keda-sh",
17+
},
18+
Spec: httpv1alpha1.HTTPScaledObjectSpec{
19+
Hosts: []string{
20+
"keda.sh",
21+
},
22+
},
23+
}
24+
25+
httpso0NamespacedName = k8s.NamespacedNameFromObject(httpso0)
26+
httpso0IndexKey = newTableMemoryIndexKey(httpso0NamespacedName)
27+
28+
httpso1 = &httpv1alpha1.HTTPScaledObject{
29+
ObjectMeta: metav1.ObjectMeta{
30+
Name: "one-one-one-one",
31+
},
32+
Spec: httpv1alpha1.HTTPScaledObjectSpec{
33+
Hosts: []string{
34+
"1.1.1.1",
35+
},
36+
},
37+
}
38+
httpso1NamespacedName = k8s.NamespacedNameFromObject(httpso1)
39+
httpso1IndexKey = newTableMemoryIndexKey(httpso1NamespacedName)
40+
)
41+
Context("New", func() {
42+
It("returns a httpSOIndex with initialized tree", func() {
43+
index := newHTTPSOIndex()
44+
Expect(index.radix).NotTo(BeNil())
45+
})
46+
})
47+
48+
Context("Get / Insert", func() {
49+
It("Get on empty httpSOIndex returns nil", func() {
50+
index := newHTTPSOIndex()
51+
_, ok := index.get(httpso0IndexKey)
52+
Expect(ok).To(BeFalse())
53+
})
54+
It("httpSOIndex insert will return previous object if set", func() {
55+
index := newHTTPSOIndex()
56+
index, prevVal, prevSet := index.insert(httpso0IndexKey, httpso0)
57+
Expect(prevSet).To(BeFalse())
58+
Expect(prevVal).To(BeNil())
59+
httpso0Copy := httpso0.DeepCopy()
60+
httpso0Copy.Name = "httpso0Copy"
61+
index, prevVal, prevSet = index.insert(httpso0IndexKey, httpso0Copy)
62+
Expect(prevSet).To(BeTrue())
63+
Expect(prevVal).To(Equal(httpso0))
64+
Expect(prevVal).ToNot(Equal(httpso0Copy))
65+
httpso, ok := index.get(httpso0IndexKey)
66+
Expect(ok).To(BeTrue())
67+
Expect(httpso).ToNot(Equal(httpso0))
68+
Expect(httpso).To(Equal(httpso0Copy))
69+
})
70+
71+
It("httpSOIndex with new object inserted returns object", func() {
72+
index := newHTTPSOIndex()
73+
index, httpso, prevSet := index.insert(httpso0IndexKey, httpso0)
74+
Expect(prevSet).To(BeFalse())
75+
Expect(httpso).To(BeNil())
76+
httpso, ok := index.get(httpso0IndexKey)
77+
Expect(ok).To(BeTrue())
78+
Expect(httpso).To(Equal(httpso0))
79+
})
80+
81+
It("httpSOIndex with new object inserted retains other object", func() {
82+
index := newHTTPSOIndex()
83+
84+
index, _, _ = index.insert(httpso0IndexKey, httpso0)
85+
httpso, ok := index.get(httpso0IndexKey)
86+
Expect(ok).To(BeTrue())
87+
Expect(httpso).To(Equal(httpso0))
88+
89+
_, ok = index.get(httpso1IndexKey)
90+
Expect(ok).To(BeFalse())
91+
92+
index, _, _ = index.insert(httpso1IndexKey, httpso1)
93+
httpso, ok = index.get(httpso1IndexKey)
94+
Expect(ok).To(BeTrue())
95+
Expect(httpso).To(Equal(httpso1))
96+
97+
// httpso0 still there
98+
httpso, ok = index.get(httpso0IndexKey)
99+
Expect(ok).To(BeTrue())
100+
Expect(httpso).To(Equal(httpso0))
101+
})
102+
})
103+
104+
Context("Get / Delete", func() {
105+
It("delete on empty httpSOIndex returns nil", func() {
106+
index := newHTTPSOIndex()
107+
_, httpso, oldSet := index.delete(httpso0IndexKey)
108+
Expect(httpso).To(BeNil())
109+
Expect(oldSet).To(BeFalse())
110+
})
111+
112+
It("double delete returns nil the second time", func() {
113+
index := newHTTPSOIndex()
114+
index, _, _ = index.insert(httpso0IndexKey, httpso0)
115+
index, _, _ = index.insert(httpso1IndexKey, httpso1)
116+
index, deletedVal, oldSet := index.delete(httpso0IndexKey)
117+
Expect(deletedVal).To(Equal(httpso0))
118+
Expect(oldSet).To(BeTrue())
119+
_, deletedVal, oldSet = index.delete(httpso0IndexKey)
120+
Expect(deletedVal).To(BeNil())
121+
Expect(oldSet).To(BeFalse())
122+
})
123+
124+
It("delete on httpSOIndex removes object ", func() {
125+
index := newHTTPSOIndex()
126+
index, _, _ = index.insert(httpso0IndexKey, httpso0)
127+
httpso, ok := index.get(httpso0IndexKey)
128+
Expect(ok).To(BeTrue())
129+
Expect(httpso).To(Equal(httpso0))
130+
index, deletedVal, oldSet := index.delete(httpso0IndexKey)
131+
Expect(deletedVal).To(Equal(httpso0))
132+
Expect(oldSet).To(BeTrue())
133+
httpso, ok = index.get(httpso0IndexKey)
134+
Expect(httpso).To(BeNil())
135+
Expect(ok).To(BeFalse())
136+
})
137+
138+
It("httpSOIndex delete on one object does not affect other", func() {
139+
index := newHTTPSOIndex()
140+
141+
index, _, _ = index.insert(httpso0IndexKey, httpso0)
142+
index, _, _ = index.insert(httpso1IndexKey, httpso1)
143+
httpso, ok := index.get(httpso0IndexKey)
144+
Expect(ok).To(BeTrue())
145+
Expect(httpso).To(Equal(httpso0))
146+
index, deletedVal, oldSet := index.delete(httpso1IndexKey)
147+
Expect(deletedVal).To(Equal(httpso1))
148+
Expect(oldSet).To(BeTrue())
149+
httpso, ok = index.get(httpso0IndexKey)
150+
Expect(ok).To(BeTrue())
151+
Expect(httpso).To(Equal(httpso0))
152+
httpso, ok = index.get(httpso1IndexKey)
153+
Expect(ok).To(BeFalse())
154+
Expect(httpso).To(BeNil())
155+
})
156+
})
157+
})

pkg/routing/httpso_store.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package routing
2+
3+
import (
4+
iradix "github.com/hashicorp/go-immutable-radix/v2"
5+
6+
httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1"
7+
"github.com/kedacore/http-add-on/pkg/k8s"
8+
)
9+
10+
// light wrapper around radix tree containing HTTPScaledObjectList
11+
// with convenience functions to manage CRUD for individual HTTPScaledObject.
12+
// created as an abstraction to manage complexity for tablememory implementation
13+
// the store is meant to map host + path keys to one or more HTTPScaledObject
14+
// and return one arbitrarily or route based on headers
15+
type httpSOStore struct {
16+
radix *iradix.Tree[httpv1alpha1.HTTPScaledObjectList]
17+
}
18+
19+
func newHTTPSOStore() *httpSOStore {
20+
return &httpSOStore{radix: iradix.New[httpv1alpha1.HTTPScaledObjectList]()}
21+
}
22+
23+
// Insert key value into httpSOStore
24+
// Gets old list of HTTPScaledObjectList
25+
// if exists appends to list and returns new httpSOStore
26+
// with new radix tree
27+
func (hs *httpSOStore) append(key Key, httpso *httpv1alpha1.HTTPScaledObject) *httpSOStore {
28+
httpsoList, found := hs.radix.Get(key)
29+
var newHTTPSOStore *httpSOStore
30+
if !found {
31+
newList := httpv1alpha1.HTTPScaledObjectList{Items: []httpv1alpha1.HTTPScaledObject{*httpso}}
32+
newRadix, _, _ := hs.radix.Insert(key, newList)
33+
newHTTPSOStore = &httpSOStore{
34+
radix: newRadix,
35+
}
36+
} else {
37+
found = false
38+
var newList httpv1alpha1.HTTPScaledObjectList
39+
for i, httpsoItem := range httpsoList.Items {
40+
if httpsoItem.Name == httpso.Name && httpsoItem.Namespace == httpso.Namespace {
41+
httpsoList.Items[i] = *httpso
42+
found = true
43+
newList = httpsoList
44+
break
45+
}
46+
}
47+
if !found {
48+
newList = httpv1alpha1.HTTPScaledObjectList{Items: append(httpsoList.Items, *httpso)}
49+
}
50+
newRadix, _, _ := hs.radix.Insert(key, newList)
51+
newHTTPSOStore = &httpSOStore{
52+
radix: newRadix,
53+
}
54+
}
55+
return newHTTPSOStore
56+
}
57+
58+
func (hs *httpSOStore) insert(key Key, httpsoList httpv1alpha1.HTTPScaledObjectList) (*httpSOStore, httpv1alpha1.HTTPScaledObjectList, bool) {
59+
newRadix, oldVal, ok := hs.radix.Insert(key, httpsoList)
60+
newHTTPSOStore := &httpSOStore{
61+
radix: newRadix,
62+
}
63+
return newHTTPSOStore, oldVal, ok
64+
}
65+
66+
func (hs *httpSOStore) get(key Key) (httpv1alpha1.HTTPScaledObjectList, bool) {
67+
return hs.radix.Get(key)
68+
}
69+
70+
func (hs *httpSOStore) delete(key Key) (*httpSOStore, httpv1alpha1.HTTPScaledObjectList, bool) {
71+
newRadix, oldVal, oldSet := hs.radix.Delete(key)
72+
newHTTPSOStore := &httpSOStore{
73+
radix: newRadix,
74+
}
75+
return newHTTPSOStore, oldVal, oldSet
76+
}
77+
78+
// convenience function
79+
// retrieves all keys associated with HTTPScaledObject
80+
// and deletes it from every list in the store
81+
func (hs *httpSOStore) DeleteAllInstancesOfHTTPSO(httpso *httpv1alpha1.HTTPScaledObject) *httpSOStore {
82+
httpsoNamespacedName := k8s.NamespacedNameFromObject(httpso)
83+
newHTTPSOStore := &httpSOStore{radix: hs.radix}
84+
keys := NewKeysFromHTTPSO(httpso)
85+
for _, key := range keys {
86+
httpsoList, _ := newHTTPSOStore.radix.Get(key)
87+
for i, httpso := range httpsoList.Items {
88+
// delete only if namespaced names match
89+
if currHttpsoNamespacedName := k8s.NamespacedNameFromObject(&httpso); *httpsoNamespacedName == *currHttpsoNamespacedName {
90+
httpsoList.Items = append(httpsoList.Items[:i], httpsoList.Items[i+1:]...)
91+
break
92+
}
93+
}
94+
if len(httpsoList.Items) == 0 {
95+
newHTTPSOStore.radix, _, _ = newHTTPSOStore.radix.Delete(key)
96+
} else {
97+
newHTTPSOStore.radix, _, _ = newHTTPSOStore.radix.Insert(key, httpsoList)
98+
}
99+
}
100+
return newHTTPSOStore
101+
}
102+
103+
func (hs *httpSOStore) GetLongestPrefix(key Key) ([]byte, httpv1alpha1.HTTPScaledObjectList, bool) {
104+
return hs.radix.Root().LongestPrefix(key)
105+
}

0 commit comments

Comments
 (0)