Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for http header routing #1222

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 15 additions & 1 deletion operator/apis/http/v1alpha1/httpscaledobject_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions operator/apis/http/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions pkg/routing/httpso_index.go
Original file line number Diff line number Diff line change
@@ -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
}
156 changes: 156 additions & 0 deletions pkg/routing/httpso_index_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
})
})
92 changes: 92 additions & 0 deletions pkg/routing/httpso_store.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading