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 5 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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
##################################################
SHELL = /bin/bash

IMAGE_REGISTRY ?= ghcr.io
IMAGE_REGISTRY ?= navigation-docker.artifactory.teslamotors.com
IMAGE_REPO ?= kedacore
VERSION ?= main
VERSION ?= header-based-routing

IMAGE_OPERATOR ?= ${IMAGE_REGISTRY}/${IMAGE_REPO}/http-add-on-operator
IMAGE_INTERCEPTOR ?= ${IMAGE_REGISTRY}/${IMAGE_REPO}/http-add-on-interceptor
Expand Down
19 changes: 19 additions & 0 deletions config/crd/bases/http.keda.sh_httpscaledobjects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ spec:
items:
type: string
type: array
headers:
description: |-
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.
items:
type: object
properties:
name:
type: string
value:
type: string
required:
- name
- value
type: array
replicas:
description: (optional) Replica information
properties:
Expand Down
2 changes: 2 additions & 0 deletions interceptor/middleware/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ var _ http.Handler = (*Routing)(nil)
func (rm *Routing) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r = util.RequestWithLoggerWithName(r, "RoutingMiddleware")

// logger := util.LoggerFromContext(r.Context())
httpso := rm.routingTable.Route(r)

if httpso == nil {
if rm.isProbe(r) {
rm.probeHandler.ServeHTTP(w, r)
Expand Down
21 changes: 20 additions & 1 deletion operator/apis/http/v1alpha1/httpscaledobject_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ type RateMetricSpec struct {
Granularity metav1.Duration `json:"granularity" description:"Time granularity for rate calculation"`
}

type Header struct {
Name string `json:"name"`
Value string `json:"value"`
}

// HTTPScaledObjectSpec defines the desired state of HTTPScaledObject
type HTTPScaledObjectSpec struct {
// The hosts to route. All requests which the "Host" header
Expand All @@ -89,6 +94,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 []Header `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 +159,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
28 changes: 26 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())
})
})
})
Loading