Skip to content

Commit 8fa1e77

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

File tree

10 files changed

+672
-113
lines changed

10 files changed

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

pkg/routing/httpso_index_test.go

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

pkg/routing/httpso_store.go

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

0 commit comments

Comments
 (0)