diff --git a/CHANGELOG.md b/CHANGELOG.md index 784230c9f..53b2d1af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ This changelog keeps track of work items that have been completed and are ready ### Improvements -- **General**: TODO ([#TODO](https://github.com/kedacore/http-add-on/issues/TODO)) +- **General**: Support for http header routing ([#1177](https://github.com/kedacore/http-add-on/issues/1177)) ### Fixes diff --git a/operator/apis/http/v1alpha1/httpscaledobject_types.go b/operator/apis/http/v1alpha1/httpscaledobject_types.go index 9bacfa080..24d45f597 100644 --- a/operator/apis/http/v1alpha1/httpscaledobject_types.go +++ b/operator/apis/http/v1alpha1/httpscaledobject_types.go @@ -89,6 +89,14 @@ type HTTPScaledObjectSpec struct { // the scaleTargetRef. // +optional PathPrefixes []string `json:"pathPrefixes,omitempty"` + // The custom headers used to route. Once Hosts and PathPrefixes have been matched, + // if at least one header in the http request matches at least one header + // in .spec.headers, it will be routed to the Service and Port specified in + // the scaleTargetRef. First header it matches with, it will be routed to. + // If the headers can't be matched, then use first one without .spec.headers supplied + // If that doesn't exist then routing will fail. + // +optional + Headers map[string]string `json:"headers,omitempty"` // The name of the deployment to route HTTP requests to (and to autoscale). // Including validation as a requirement to define either the PortName or the Port // +kubebuilder:validation:XValidation:rule="has(self.portName) != has(self.port)",message="must define either the 'portName' or the 'port'" @@ -146,7 +154,13 @@ type HTTPScaledObject struct { type HTTPScaledObjectList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []HTTPScaledObject `json:"items"` + Items []*HTTPScaledObject `json:"items"` +} + +func NewHTTPScaledObjectList(httpScaledObjects []*HTTPScaledObject) *HTTPScaledObjectList { + return &HTTPScaledObjectList{ + Items: httpScaledObjects, + } } func init() { diff --git a/operator/apis/http/v1alpha1/zz_generated.deepcopy.go b/operator/apis/http/v1alpha1/zz_generated.deepcopy.go index 27d772640..98dded39b 100644 --- a/operator/apis/http/v1alpha1/zz_generated.deepcopy.go +++ b/operator/apis/http/v1alpha1/zz_generated.deepcopy.go @@ -107,9 +107,9 @@ func (in *HTTPScaledObjectList) DeepCopyInto(out *HTTPScaledObjectList) { in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]HTTPScaledObject, len(*in)) + *out = make([]*HTTPScaledObject, len(*in)) for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) + (*in)[i].DeepCopyInto((*out)[i]) } } } diff --git a/pkg/routing/httpso_index.go b/pkg/routing/httpso_index.go new file mode 100644 index 000000000..312bab916 --- /dev/null +++ b/pkg/routing/httpso_index.go @@ -0,0 +1,34 @@ +package routing + +import ( + iradix "github.com/hashicorp/go-immutable-radix/v2" + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" +) + +type httpSOIndex struct { + radix *iradix.Tree[*httpv1alpha1.HTTPScaledObject] +} + +func newHTTPSOIndex() *httpSOIndex { + return &httpSOIndex{radix: iradix.New[*httpv1alpha1.HTTPScaledObject]()} +} + +func (hi *httpSOIndex) insert(key tableMemoryIndexKey, httpso *httpv1alpha1.HTTPScaledObject) (*httpSOIndex, *httpv1alpha1.HTTPScaledObject, bool) { + newRadix, oldVal, oldSet := hi.radix.Insert(key, httpso) + newHttpSOIndex := &httpSOIndex{ + radix: newRadix, + } + return newHttpSOIndex, oldVal, oldSet +} + +func (hi *httpSOIndex) get(key tableMemoryIndexKey) (*httpv1alpha1.HTTPScaledObject, bool) { + return hi.radix.Get(key) +} + +func (hi *httpSOIndex) delete(key tableMemoryIndexKey) (*httpSOIndex, *httpv1alpha1.HTTPScaledObject, bool) { + newRadix, oldVal, oldSet := hi.radix.Delete(key) + newHttpSOIndex := &httpSOIndex{ + radix: newRadix, + } + return newHttpSOIndex, oldVal, oldSet +} diff --git a/pkg/routing/httpso_index_test.go b/pkg/routing/httpso_index_test.go new file mode 100644 index 000000000..7ec441e70 --- /dev/null +++ b/pkg/routing/httpso_index_test.go @@ -0,0 +1,156 @@ +package routing + +import ( + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + "github.com/kedacore/http-add-on/pkg/k8s" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("httpSOIndex", func() { + var ( + httpso0 = &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keda-sh", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "keda.sh", + }, + }, + } + + httpso0NamespacedName = k8s.NamespacedNameFromObject(httpso0) + httpso0IndexKey = newTableMemoryIndexKey(httpso0NamespacedName) + + httpso1 = &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "one-one-one-one", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "1.1.1.1", + }, + }, + } + httpso1NamespacedName = k8s.NamespacedNameFromObject(httpso1) + httpso1IndexKey = newTableMemoryIndexKey(httpso1NamespacedName) + ) + Context("New", func() { + It("returns a httpSOIndex with initialized tree", func() { + index := newHTTPSOIndex() + Expect(index.radix).NotTo(BeNil()) + }) + }) + + Context("Get / Insert", func() { + It("Get on empty httpSOIndex returns nil", func() { + index := newHTTPSOIndex() + _, ok := index.get(httpso0IndexKey) + Expect(ok).To(BeFalse()) + }) + It("httpSOIndex insert will return previous object if set", func() { + index := newHTTPSOIndex() + index, prevVal, prevSet := index.insert(httpso0IndexKey, httpso0) + Expect(prevSet).To(BeFalse()) + Expect(prevVal).To(BeNil()) + httpso0Copy := httpso0.DeepCopy() + httpso0Copy.Name = "httpso0Copy" + index, prevVal, prevSet = index.insert(httpso0IndexKey, httpso0Copy) + Expect(prevSet).To(BeTrue()) + Expect(prevVal).To(Equal(httpso0)) + Expect(prevVal).ToNot(Equal(httpso0Copy)) + httpso, ok := index.get(httpso0IndexKey) + Expect(ok).To(BeTrue()) + Expect(httpso).ToNot(Equal(httpso0)) + Expect(httpso).To(Equal(httpso0Copy)) + }) + + It("httpSOIndex with new object inserted returns object", func() { + index := newHTTPSOIndex() + index, httpso, prevSet := index.insert(httpso0IndexKey, httpso0) + Expect(prevSet).To(BeFalse()) + Expect(httpso).To(BeNil()) + httpso, ok := index.get(httpso0IndexKey) + Expect(ok).To(BeTrue()) + Expect(httpso).To(Equal(httpso0)) + }) + + It("httpSOIndex with new object inserted retains other object", func() { + index := newHTTPSOIndex() + + index, _, _ = index.insert(httpso0IndexKey, httpso0) + httpso, ok := index.get(httpso0IndexKey) + Expect(ok).To(BeTrue()) + Expect(httpso).To(Equal(httpso0)) + + _, ok = index.get(httpso1IndexKey) + Expect(ok).To(BeFalse()) + + index, _, _ = index.insert(httpso1IndexKey, httpso1) + httpso, ok = index.get(httpso1IndexKey) + Expect(ok).To(BeTrue()) + Expect(httpso).To(Equal(httpso1)) + + // httpso0 still there + httpso, ok = index.get(httpso0IndexKey) + Expect(ok).To(BeTrue()) + Expect(httpso).To(Equal(httpso0)) + }) + }) + + Context("Get / Delete", func() { + It("delete on empty httpSOIndex returns nil", func() { + index := newHTTPSOIndex() + _, httpso, oldSet := index.delete(httpso0IndexKey) + Expect(httpso).To(BeNil()) + Expect(oldSet).To(BeFalse()) + }) + + It("double delete returns nil the second time", func() { + index := newHTTPSOIndex() + index, _, _ = index.insert(httpso0IndexKey, httpso0) + index, _, _ = index.insert(httpso1IndexKey, httpso1) + index, deletedVal, oldSet := index.delete(httpso0IndexKey) + Expect(deletedVal).To(Equal(httpso0)) + Expect(oldSet).To(BeTrue()) + index, deletedVal, oldSet = index.delete(httpso0IndexKey) + Expect(deletedVal).To(BeNil()) + Expect(oldSet).To(BeFalse()) + }) + + It("delete on httpSOIndex removes object ", func() { + index := newHTTPSOIndex() + index, _, _ = index.insert(httpso0IndexKey, httpso0) + httpso, ok := index.get(httpso0IndexKey) + Expect(ok).To(BeTrue()) + Expect(httpso).To(Equal(httpso0)) + index, deletedVal, oldSet := index.delete(httpso0IndexKey) + Expect(deletedVal).To(Equal(httpso0)) + Expect(oldSet).To(BeTrue()) + httpso, ok = index.get(httpso0IndexKey) + Expect(httpso).To(BeNil()) + Expect(ok).To(BeFalse()) + }) + + It("httpSOIndex delete on one object does not affect other", func() { + index := newHTTPSOIndex() + + index, _, _ = index.insert(httpso0IndexKey, httpso0) + index, _, _ = index.insert(httpso1IndexKey, httpso1) + httpso, ok := index.get(httpso0IndexKey) + Expect(ok).To(BeTrue()) + Expect(httpso).To(Equal(httpso0)) + index, deletedVal, oldSet := index.delete(httpso1IndexKey) + Expect(deletedVal).To(Equal(httpso1)) + Expect(oldSet).To(BeTrue()) + httpso, ok = index.get(httpso0IndexKey) + Expect(ok).To(BeTrue()) + Expect(httpso).To(Equal(httpso0)) + httpso, ok = index.get(httpso1IndexKey) + Expect(ok).To(BeFalse()) + Expect(httpso).To(BeNil()) + }) + }) +}) diff --git a/pkg/routing/httpso_store.go b/pkg/routing/httpso_store.go new file mode 100644 index 000000000..a4c615d3f --- /dev/null +++ b/pkg/routing/httpso_store.go @@ -0,0 +1,92 @@ +package routing + +import ( + iradix "github.com/hashicorp/go-immutable-radix/v2" + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + "github.com/kedacore/http-add-on/pkg/k8s" +) + +// light wrapper around radix tree containing HTTPScaledObjectList +// with convenience functions to manage CRUD for individual HTTPScaledObject. +// created as an abstraction to manage complexity for tablememory implementation +// the store is meant to map host + path keys to one or more HTTPScaledObject +// and return one arbitrarily or route based on headers +type httpSOStore struct { + radix *iradix.Tree[*httpv1alpha1.HTTPScaledObjectList] +} + +func newHTTPSOStore() *httpSOStore { + return &httpSOStore{radix: iradix.New[*httpv1alpha1.HTTPScaledObjectList]()} +} + +// Insert key value into httpSOStore +// Gets old list of HTTPScaledObjectList +// if exists appends to list and returns new httpSOStore +// with new radix tree +func (hs *httpSOStore) append(key Key, httpso *httpv1alpha1.HTTPScaledObject) *httpSOStore { + httpsoList, found := hs.radix.Get(key) + var newHttpSOStore *httpSOStore + if !found { + newList := &httpv1alpha1.HTTPScaledObjectList{Items: []*httpv1alpha1.HTTPScaledObject{httpso}} + newRadix, _, _ := hs.radix.Insert(key, newList) + newHttpSOStore = &httpSOStore{ + radix: newRadix, + } + } else { + newList := &httpv1alpha1.HTTPScaledObjectList{Items: append(httpsoList.Items, httpso)} + newRadix, _, _ := hs.radix.Insert(key, newList) + newHttpSOStore = &httpSOStore{ + radix: newRadix, + } + } + return newHttpSOStore +} + +func (hs *httpSOStore) insert(key Key, httpsoList *httpv1alpha1.HTTPScaledObjectList) (*httpSOStore, *httpv1alpha1.HTTPScaledObjectList, bool) { + newRadix, oldVal, ok := hs.radix.Insert(key, httpsoList) + newHttpSOStore := &httpSOStore{ + radix: newRadix, + } + return newHttpSOStore, oldVal, ok +} + +func (hs *httpSOStore) get(key Key) (*httpv1alpha1.HTTPScaledObjectList, bool) { + return hs.radix.Get(key) +} + +func (hs *httpSOStore) delete(key Key) (*httpSOStore, *httpv1alpha1.HTTPScaledObjectList, bool) { + newRadix, oldVal, oldSet := hs.radix.Delete(key) + newHttpSOStore := &httpSOStore{ + radix: newRadix, + } + return newHttpSOStore, oldVal, oldSet +} + +// convenience function +// retrieves all keys associated with HTTPScaledObject +// and deletes it from every list in the store +func (hs *httpSOStore) DeleteAllInstancesOfHTTPSO(httpso *httpv1alpha1.HTTPScaledObject) *httpSOStore { + httpsoNamespacedName := k8s.NamespacedNameFromObject(httpso) + newHttpSOStore := &httpSOStore{radix: hs.radix} + keys := NewKeysFromHTTPSO(httpso) + for _, key := range keys { + httpsoList, _ := newHttpSOStore.radix.Get(key) + for i, httpso := range httpsoList.Items { + // delete only if namespaced names match + if currHttpsoNamespacedName := k8s.NamespacedNameFromObject(httpso); *httpsoNamespacedName == *currHttpsoNamespacedName { + httpsoList.Items = append(httpsoList.Items[:i], httpsoList.Items[i+1:]...) + break + } + } + if len(httpsoList.Items) == 0 { + newHttpSOStore.radix, _, _ = newHttpSOStore.radix.Delete(key) + } else { + newHttpSOStore.radix, _, _ = newHttpSOStore.radix.Insert(key, httpsoList) + } + } + return newHttpSOStore +} + +func (hs *httpSOStore) GetLongestPrefix(key Key) ([]byte, *httpv1alpha1.HTTPScaledObjectList, bool) { + return hs.radix.Root().LongestPrefix(key) +} diff --git a/pkg/routing/httpso_store_test.go b/pkg/routing/httpso_store_test.go new file mode 100644 index 000000000..310ab987e --- /dev/null +++ b/pkg/routing/httpso_store_test.go @@ -0,0 +1,233 @@ +package routing + +import ( + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("httpSOStore", func() { + var ( + httpso0 = &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keda-sh", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "keda.sh", + }, + }, + } + httpso0List = &httpv1alpha1.HTTPScaledObjectList{ + Items: []*httpv1alpha1.HTTPScaledObject{ + httpso0, + }, + } + + httpso0StoreKeys = NewKeysFromHTTPSO(httpso0) + httpso1 = &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "one-one-one-one", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "1.1.1.1", + }, + }, + } + httpso1List = &httpv1alpha1.HTTPScaledObjectList{ + Items: []*httpv1alpha1.HTTPScaledObject{ + httpso1, + }, + } + httpso1StoreKeys = NewKeysFromHTTPSO(httpso1) + ) + Context("New", func() { + It("returns a httpSOStore with initialized tree", func() { + store := newHTTPSOStore() + Expect(store.radix).NotTo(BeNil()) + }) + }) + + Context("Get / Insert", func() { + It("Get on empty httpSOStore returns nil", func() { + store := newHTTPSOStore() + for _, key := range httpso0StoreKeys { + _, ok := store.get(key) + Expect(ok).To(BeFalse()) + } + }) + + It("httpSOStore with new object inserted returns old object", func() { + store := newHTTPSOStore() + for _, key := range httpso0StoreKeys { + var prevVal *httpv1alpha1.HTTPScaledObjectList + var prevSet bool + store, prevVal, prevSet = store.insert(key, httpso0List) + Expect(prevVal).To(BeNil()) + Expect(prevSet).To(BeFalse()) + } + httpso0ListCopy := &httpv1alpha1.HTTPScaledObjectList{ + Items: httpso0List.Items, + ListMeta: metav1.ListMeta{ + ResourceVersion: "httpso0ListCopy", + }, + } + for _, key := range httpso0StoreKeys { + var prevVal *httpv1alpha1.HTTPScaledObjectList + var prevSet bool + store, prevVal, prevSet = store.insert(key, httpso0ListCopy) + Expect(prevVal).To(Equal(httpso0List)) + Expect(prevVal).ToNot(Equal(httpso0ListCopy)) + Expect(prevSet).To(BeTrue()) + } + }) + + It("httpSOStore insert will return object if set", func() { + store := newHTTPSOStore() + for _, key := range httpso0StoreKeys { + var prevVal *httpv1alpha1.HTTPScaledObjectList + var prevSet bool + store, prevVal, prevSet = store.insert(key, httpso0List) + Expect(prevVal).To(BeNil()) + Expect(prevSet).To(BeFalse()) + } + for _, key := range httpso0StoreKeys { + httpsoList, ok := store.get(key) + Expect(httpsoList).To(Equal(httpso0List)) + Expect(ok).To(BeTrue()) + } + }) + + It("httpSOStore with new object inserted retains other object", func() { + store := newHTTPSOStore() + for _, key := range httpso0StoreKeys { + store, _, _ = store.insert(key, httpso0List) + } + + for _, key := range httpso0StoreKeys { + httpsoList, ok := store.get(key) + Expect(ok).To(BeTrue()) + Expect(httpsoList).To(Equal(httpso0List)) + } + + for _, key := range httpso1StoreKeys { + httpsoList, ok := store.get(key) + Expect(ok).To(BeFalse()) + Expect(httpsoList).To(BeNil()) + } + + for _, key := range httpso1StoreKeys { + store, _, _ = store.insert(key, httpso1List) + } + + for _, key := range httpso1StoreKeys { + httpsoList, ok := store.get(key) + Expect(ok).To(BeTrue()) + Expect(httpsoList).To(Equal(httpso1List)) + } + + for _, key := range httpso0StoreKeys { + httpsoList, ok := store.get(key) + Expect(ok).To(BeTrue()) + Expect(httpsoList).To(Equal(httpso0List)) + } + }) + }) + + Context("Get / Delete", func() { + It("delete on empty httpSOStore returns nil", func() { + store := newHTTPSOStore() + for _, key := range httpso0StoreKeys { + var prevVal *httpv1alpha1.HTTPScaledObjectList + var prevSet bool + store, prevVal, prevSet = store.delete(key) + Expect(prevVal).To(BeNil()) + Expect(prevSet).To(BeFalse()) + } + }) + + It("double delete returns nil the second time", func() { + store := newHTTPSOStore() + for _, key := range httpso0StoreKeys { + store, _, _ = store.insert(key, httpso0List) + } + for _, key := range httpso1StoreKeys { + store, _, _ = store.insert(key, httpso1List) + } + for _, key := range httpso0StoreKeys { + var prevVal *httpv1alpha1.HTTPScaledObjectList + var prevSet bool + store, prevVal, prevSet = store.delete(key) + Expect(prevVal).To(Equal(httpso0List)) + Expect(prevSet).To(BeTrue()) + } + for _, key := range httpso0StoreKeys { + var prevVal *httpv1alpha1.HTTPScaledObjectList + var prevSet bool + store, prevVal, prevSet = store.delete(key) + Expect(prevVal).To(BeNil()) + Expect(prevSet).To(BeFalse()) + } + }) + + It("delete on httpSOStore removes object ", func() { + store := newHTTPSOStore() + for _, key := range httpso0StoreKeys { + store, _, _ = store.insert(key, httpso0List) + } + for _, key := range httpso0StoreKeys { + httpsoList, ok := store.get(key) + Expect(ok).To(BeTrue()) + Expect(httpsoList).To(Equal(httpso0List)) + } + + for _, key := range httpso0StoreKeys { + var prevVal *httpv1alpha1.HTTPScaledObjectList + var prevSet bool + store, prevVal, prevSet = store.delete(key) + Expect(prevVal).To(Equal(httpso0List)) + Expect(prevSet).To(BeTrue()) + } + for _, key := range httpso0StoreKeys { + httpsoList, ok := store.get(key) + Expect(httpsoList).To(BeNil()) + Expect(ok).To(BeFalse()) + } + }) + + It("httpSOStore delete on one object does not affect other", func() { + store := newHTTPSOStore() + for _, key := range httpso0StoreKeys { + store, _, _ = store.insert(key, httpso0List) + } + for _, key := range httpso1StoreKeys { + store, _, _ = store.insert(key, httpso1List) + } + for _, key := range httpso0StoreKeys { + httpsoList, ok := store.get(key) + Expect(ok).To(BeTrue()) + Expect(httpsoList).To(Equal(httpso0List)) + } + for _, key := range httpso1StoreKeys { + var prevVal *httpv1alpha1.HTTPScaledObjectList + var prevSet bool + store, prevVal, prevSet = store.delete(key) + Expect(prevVal).To(Equal(httpso1List)) + Expect(prevSet).To(BeTrue()) + } + + for _, key := range httpso0StoreKeys { + httpsoList, ok := store.get(key) + Expect(ok).To(BeTrue()) + Expect(httpsoList).To(Equal(httpso0List)) + } + for _, key := range httpso1StoreKeys { + httpsoList, ok := store.get(key) + Expect(ok).To(BeFalse()) + Expect(httpsoList).To(BeNil()) + } + }) + }) +}) diff --git a/pkg/routing/table.go b/pkg/routing/table.go index 4cf8a5309..1393ec802 100644 --- a/pkg/routing/table.go +++ b/pkg/routing/table.go @@ -135,7 +135,7 @@ func (t *table) Route(req *http.Request) *httpv1alpha1.HTTPScaledObject { } key := NewKeyFromRequest(req) - return tm.Route(key) + return tm.RouteWithHeaders(key, req.Header) } func (t *table) HasSynced() bool { diff --git a/pkg/routing/table_test.go b/pkg/routing/table_test.go index e0bbf478b..bd7a301aa 100644 --- a/pkg/routing/table_test.go +++ b/pkg/routing/table_test.go @@ -33,7 +33,7 @@ var _ = Describe("Table", func() { ctx = context.Background() httpsoList = httpv1alpha1.HTTPScaledObjectList{ - Items: []httpv1alpha1.HTTPScaledObject{ + Items: []*httpv1alpha1.HTTPScaledObject{ { ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, @@ -192,8 +192,8 @@ var _ = Describe("Table", func() { for _, httpso := range httpsoList.Items { httpso := httpso - key := *k8s.NamespacedNameFromObject(&httpso) - t.httpScaledObjects[key] = &httpso + key := *k8s.NamespacedNameFromObject(httpso) + t.httpScaledObjects[key] = httpso } go util.IgnoringError(util.ApplyContext(t.runInformer, ctx)) @@ -205,9 +205,9 @@ var _ = Describe("Table", func() { Expect(tm).NotTo(BeNil()) for _, httpso := range httpsoList.Items { - namespacedName := k8s.NamespacedNameFromObject(&httpso) + namespacedName := k8s.NamespacedNameFromObject(httpso) ret := tm.Recall(namespacedName) - Expect(ret).To(Equal(&httpso)) + Expect(ret).To(Equal(httpso)) } }) @@ -218,8 +218,8 @@ var _ = Describe("Table", func() { for _, httpso := range httpsoList.Items { httpso := httpso - key := *k8s.NamespacedNameFromObject(&httpso) - t.httpScaledObjects[key] = &httpso + key := *k8s.NamespacedNameFromObject(httpso) + t.httpScaledObjects[key] = httpso } go util.IgnoringError(util.ApplyContext(t.runInformer, ctx)) @@ -227,7 +227,7 @@ var _ = Describe("Table", func() { time.Sleep(2 * time.Second) - httpso := httpv1alpha1.HTTPScaledObject{ + httpso := &httpv1alpha1.HTTPScaledObject{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: "azure-com", @@ -241,10 +241,10 @@ var _ = Describe("Table", func() { }, }, } - t.httpScaledObjects[*k8s.NamespacedNameFromObject(&httpso)] = &httpso + t.httpScaledObjects[*k8s.NamespacedNameFromObject(httpso)] = httpso first := httpsoList.Items[0] - delete(t.httpScaledObjects, *k8s.NamespacedNameFromObject(&first)) + delete(t.httpScaledObjects, *k8s.NamespacedNameFromObject(first)) t.memorySignaler.Signal() @@ -254,12 +254,12 @@ var _ = Describe("Table", func() { Expect(tm).NotTo(BeNil()) for _, httpso := range append(httpsoList.Items[1:], httpso) { - namespacedName := k8s.NamespacedNameFromObject(&httpso) + namespacedName := k8s.NamespacedNameFromObject(httpso) ret := tm.Recall(namespacedName) - Expect(ret).To(Equal(&httpso)) + Expect(ret).To(Equal(httpso)) } - namespacedName := k8s.NamespacedNameFromObject(&first) + namespacedName := k8s.NamespacedNameFromObject(first) ret := tm.Recall(namespacedName) Expect(ret).To(BeNil()) }) @@ -287,17 +287,17 @@ var _ = Describe("Table", func() { for _, httpso := range httpsoList.Items { httpso := httpso - key := *k8s.NamespacedNameFromObject(&httpso) - t.httpScaledObjects[key] = &httpso + key := *k8s.NamespacedNameFromObject(httpso) + t.httpScaledObjects[key] = httpso } tm := t.newMemoryFromHTTPSOs() for _, httpso := range httpsoList.Items { - namespacedName := k8s.NamespacedNameFromObject(&httpso) + namespacedName := k8s.NamespacedNameFromObject(httpso) ret := tm.Recall(namespacedName) - Expect(ret).To(Equal(&httpso)) + Expect(ret).To(Equal(httpso)) } }) }) diff --git a/pkg/routing/tablememory.go b/pkg/routing/tablememory.go index 0cb0d8f10..e53116aa9 100644 --- a/pkg/routing/tablememory.go +++ b/pkg/routing/tablememory.go @@ -1,7 +1,6 @@ package routing import ( - iradix "github.com/hashicorp/go-immutable-radix/v2" "k8s.io/apimachinery/pkg/types" httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" @@ -13,17 +12,25 @@ type TableMemory interface { Recall(namespacedName *types.NamespacedName) *httpv1alpha1.HTTPScaledObject Forget(namespacedName *types.NamespacedName) TableMemory Route(key Key) *httpv1alpha1.HTTPScaledObject + RouteWithHeaders(key Key, httpHeaders map[string][]string) *httpv1alpha1.HTTPScaledObject } type tableMemory struct { - index *iradix.Tree[*httpv1alpha1.HTTPScaledObject] - store *iradix.Tree[*httpv1alpha1.HTTPScaledObject] + index *httpSOIndex + store *httpSOStore +} + +func newTableMemory() tableMemory { + return tableMemory{ + index: newHTTPSOIndex(), + store: newHTTPSOStore(), + } } func NewTableMemory() TableMemory { return tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + index: newHTTPSOIndex(), + store: newHTTPSOStore(), } } @@ -36,21 +43,13 @@ func (tm tableMemory) Remember(httpso *httpv1alpha1.HTTPScaledObject) TableMemor httpso = httpso.DeepCopy() indexKey := newTableMemoryIndexKeyFromHTTPSO(httpso) - index, _, _ := tm.index.Insert(indexKey, httpso) + index, _, _ := tm.index.insert(indexKey, httpso) keys := NewKeysFromHTTPSO(httpso) store := tm.store for _, key := range keys { - newStore, oldHTTPSO, _ := store.Insert(key, httpso) - - // oldest HTTPScaledObject has precedence - if oldHTTPSO != nil && httpso.GetCreationTimestamp().Time.After(oldHTTPSO.GetCreationTimestamp().Time) { - continue - } - - store = newStore + store = store.append(key, httpso) } - return tableMemory{ index: index, store: store, @@ -63,7 +62,7 @@ func (tm tableMemory) Recall(namespacedName *types.NamespacedName) *httpv1alpha1 } indexKey := newTableMemoryIndexKey(namespacedName) - httpso, _ := tm.index.Get(indexKey) + httpso, _ := tm.index.get(indexKey) if httpso == nil { return nil } @@ -75,26 +74,12 @@ func (tm tableMemory) Forget(namespacedName *types.NamespacedName) TableMemory { if namespacedName == nil { return nil } - indexKey := newTableMemoryIndexKey(namespacedName) - index, httpso, _ := tm.index.Delete(indexKey) - if httpso == nil { + index, httpso, oldSet := tm.index.delete(indexKey) + if httpso == nil || oldSet == false { return tm } - - keys := NewKeysFromHTTPSO(httpso) - store := tm.store - for _, key := range keys { - newStore, oldHTTPSO, _ := store.Delete(key) - - // delete only if namespaced names match - if oldNamespacedName := k8s.NamespacedNameFromObject(oldHTTPSO); oldNamespacedName == nil || *oldNamespacedName != *namespacedName { - continue - } - - store = newStore - } - + store := tm.store.DeleteAllInstancesOfHTTPSO(httpso) return tableMemory{ index: index, store: store, @@ -102,8 +87,46 @@ func (tm tableMemory) Forget(namespacedName *types.NamespacedName) TableMemory { } func (tm tableMemory) Route(key Key) *httpv1alpha1.HTTPScaledObject { - _, httpso, _ := tm.store.Root().LongestPrefix(key) - return httpso + _, httpsoList, _ := tm.store.GetLongestPrefix(key) + if httpsoList == nil || len(httpsoList.Items) == 0 { + return nil + } + return httpsoList.Items[0] +} + +func (tm tableMemory) RouteWithHeaders(key Key, httpHeaders map[string][]string) *httpv1alpha1.HTTPScaledObject { + _, httpsoList, _ := tm.store.GetLongestPrefix(key) + if httpsoList == nil || len(httpsoList.Items) == 0 { + return nil + } + if httpHeaders == nil || len(httpHeaders) == 0 { + return httpsoList.Items[0] + } + var httpsoWithoutHeaders *httpv1alpha1.HTTPScaledObject + // route to first httpso which has a matching header + for _, httpso := range httpsoList.Items { + if httpso.Spec.Headers != nil { + for k, v1 := range httpso.Spec.Headers { + if headerValues, exists := httpHeaders[k]; exists { + for _, v2 := range headerValues { + if v1 == v2 { + return httpso + } + } + } + } + } else if httpsoWithoutHeaders == nil { + httpsoWithoutHeaders = httpso + } + } + + // if no matches via header, route to httpso without headers supplied + if httpsoWithoutHeaders != nil { + return httpsoWithoutHeaders + } + + // otherwise routing fails + return nil } type tableMemoryIndexKey []byte diff --git a/pkg/routing/tablememory_test.go b/pkg/routing/tablememory_test.go index 79eaac041..f1d5febf3 100644 --- a/pkg/routing/tablememory_test.go +++ b/pkg/routing/tablememory_test.go @@ -5,7 +5,6 @@ import ( "net/url" "time" - iradix "github.com/hashicorp/go-immutable-radix/v2" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -49,7 +48,7 @@ var _ = Describe("TableMemory", func() { httpso1NamespacedName = *k8s.NamespacedNameFromObject(&httpso1) httpsoList = httpv1alpha1.HTTPScaledObjectList{ - Items: []httpv1alpha1.HTTPScaledObject{ + Items: []*httpv1alpha1.HTTPScaledObject{ { ObjectMeta: metav1.ObjectMeta{ Name: "/", @@ -118,7 +117,7 @@ var _ = Describe("TableMemory", func() { namespacedName := k8s.NamespacedNameFromObject(input) indexKey := newTableMemoryIndexKey(namespacedName) - httpso, ok := tm.index.Get(indexKey) + httpso, ok := tm.index.get(indexKey) Expect(ok).To(okMatcher) Expect(httpso).To(httpsoMatcher) } @@ -129,16 +128,20 @@ var _ = Describe("TableMemory", func() { okMatcher = BeFalse() } - httpsoMatcher := Equal(expected) + httpsoMatcher := ContainElements(expected) if expected == nil { httpsoMatcher = BeNil() } storeKeys := NewKeysFromHTTPSO(input) for _, storeKey := range storeKeys { - httpso, ok := tm.store.Get(storeKey) + httpSOList, ok := tm.store.get(storeKey) Expect(ok).To(okMatcher) - Expect(httpso).To(httpsoMatcher) + if httpSOList == nil { + Expect(httpSOList).To(httpsoMatcher) + } else { + Expect(httpSOList.Items).To(httpsoMatcher) + } } } @@ -150,7 +153,7 @@ var _ = Describe("TableMemory", func() { insertIndex = func(tm tableMemory, httpso *httpv1alpha1.HTTPScaledObject) tableMemory { namespacedName := k8s.NamespacedNameFromObject(httpso) indexKey := newTableMemoryIndexKey(namespacedName) - tm.index, _, _ = tm.index.Insert(indexKey, httpso) + tm.index, _, _ = tm.index.insert(indexKey, httpso) return tm } @@ -158,9 +161,8 @@ var _ = Describe("TableMemory", func() { insertStore = func(tm tableMemory, httpso *httpv1alpha1.HTTPScaledObject) tableMemory { storeKeys := NewKeysFromHTTPSO(httpso) for _, storeKey := range storeKeys { - tm.store, _, _ = tm.store.Insert(storeKey, httpso) + tm.store, _, _ = tm.store.insert(storeKey, httpv1alpha1.NewHTTPScaledObjectList([]*httpv1alpha1.HTTPScaledObject{httpso})) } - return tm } @@ -184,20 +186,14 @@ var _ = Describe("TableMemory", func() { Context("Remember", func() { It("returns a tableMemory with new object inserted", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = tm.Remember(&httpso0).(tableMemory) assertTrees(tm, &httpso0, &httpso0) }) It("returns a tableMemory with new object inserted and other objects retained", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = tm.Remember(&httpso0).(tableMemory) tm = tm.Remember(&httpso1).(tableMemory) @@ -206,10 +202,7 @@ var _ = Describe("TableMemory", func() { }) It("returns a tableMemory with old object of same key replaced", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = tm.Remember(&httpso0).(tableMemory) httpso1 := *httpso0.DeepCopy() @@ -220,10 +213,7 @@ var _ = Describe("TableMemory", func() { }) It("returns a tableMemory with old object of same key replaced and other objects retained", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = tm.Remember(&httpso0).(tableMemory) tm = tm.Remember(&httpso1).(tableMemory) @@ -236,10 +226,7 @@ var _ = Describe("TableMemory", func() { }) It("returns a tableMemory with deep-copied object", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() httpso := *httpso0.DeepCopy() tm = tm.Remember(&httpso).(tableMemory) @@ -249,10 +236,7 @@ var _ = Describe("TableMemory", func() { }) It("gives precedence to the oldest object on conflict", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() t0 := time.Now() @@ -290,10 +274,7 @@ var _ = Describe("TableMemory", func() { Context("Forget", func() { It("returns a tableMemory with old object deleted", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = insertTrees(tm, &httpso0) tm = tm.Forget(&httpso0NamespacedName).(tableMemory) @@ -302,10 +283,7 @@ var _ = Describe("TableMemory", func() { }) It("returns a tableMemory with old object deleted and other objects retained", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = insertTrees(tm, &httpso0) tm = insertTrees(tm, &httpso1) @@ -316,10 +294,7 @@ var _ = Describe("TableMemory", func() { }) It("returns unchanged tableMemory when object is absent", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = insertTrees(tm, &httpso0) index0 := *tm.index @@ -332,10 +307,7 @@ var _ = Describe("TableMemory", func() { }) It("forgets only when namespaced names match on conflict", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = insertTrees(tm, &httpso0) t0 := time.Now() @@ -377,10 +349,7 @@ var _ = Describe("TableMemory", func() { Context("Recall", func() { It("returns object with matching key", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = insertTrees(tm, &httpso0) httpso := tm.Recall(&httpso0NamespacedName) @@ -388,10 +357,7 @@ var _ = Describe("TableMemory", func() { }) It("returns nil when object is absent", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = insertTrees(tm, &httpso0) httpso := tm.Recall(&httpso1NamespacedName) @@ -399,10 +365,7 @@ var _ = Describe("TableMemory", func() { }) It("returns deep-copied object", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = insertTrees(tm, &httpso0) httpso := tm.Recall(&httpso0NamespacedName) @@ -416,10 +379,7 @@ var _ = Describe("TableMemory", func() { Context("Route", func() { It("returns nil when no matching host for URL", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = insertTrees(tm, &httpso0) url, err := url.Parse(fmt.Sprintf("https://%s.br", httpso0.Spec.Hosts[0])) @@ -432,10 +392,7 @@ var _ = Describe("TableMemory", func() { }) It("returns expected object with matching host for URL", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() tm = insertTrees(tm, &httpso0) tm = insertTrees(tm, &httpso1) @@ -462,11 +419,8 @@ var _ = Describe("TableMemory", func() { httpsoFoo = httpsoList.Items[3] ) - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } - tm = insertTrees(tm, &httpsoFoo) + tm := newTableMemory() + tm = insertTrees(tm, httpsoFoo) //goland:noinspection HttpUrlsUsage url, err := url.Parse(fmt.Sprintf("http://%s/bar%s", httpsoFoo.Spec.Hosts[0], httpsoFoo.Spec.PathPrefixes[0])) @@ -479,14 +433,11 @@ var _ = Describe("TableMemory", func() { }) It("returns expected object with matching pathPrefix for URL", func() { - tm := tableMemory{ - index: iradix.New[*httpv1alpha1.HTTPScaledObject](), - store: iradix.New[*httpv1alpha1.HTTPScaledObject](), - } + tm := newTableMemory() for _, httpso := range httpsoList.Items { httpso := httpso - tm = insertTrees(tm, &httpso) + tm = insertTrees(tm, httpso) } for _, httpso := range httpsoList.Items { @@ -496,7 +447,7 @@ var _ = Describe("TableMemory", func() { urlKey := NewKeyFromURL(url) Expect(urlKey).NotTo(BeNil()) ret := tm.Route(urlKey) - Expect(ret).To(Equal(&httpso)) + Expect(ret).To(Equal(httpso)) } for _, httpso := range httpsoList.Items { @@ -506,7 +457,7 @@ var _ = Describe("TableMemory", func() { urlKey := NewKeyFromURL(url) Expect(urlKey).NotTo(BeNil()) ret := tm.Route(urlKey) - Expect(ret).To(Equal(&httpso)) + Expect(ret).To(Equal(httpso)) } }) }) @@ -579,7 +530,7 @@ var _ = Describe("TableMemory", func() { tm = tm.Remember(&httpso) ret9 := tm.Route(url1Key) - Expect(ret9).To(Equal(&httpso)) + Expect(ret9).To(Equal(&httpso1)) }) }) })