diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 89cea199d..7698bc4ed 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -54,14 +54,6 @@ RUN apt-get update \ github.com/derekparker/delve/cmd/dlv 2>&1 \ && go install honnef.co/go/tools/cmd/staticcheck@latest \ && go install golang.org/x/tools/gopls@latest \ - # Protocol Buffer Compiler - && PROTOC_VERSION=21.9 \ - && if [ $(dpkg --print-architecture) = "amd64" ]; then PROTOC_ARCH="x86_64"; else PROTOC_ARCH="aarch_64" ; fi \ - && curl -LO "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-$PROTOC_ARCH.zip" \ - && unzip "protoc-${PROTOC_VERSION}-linux-$PROTOC_ARCH.zip" -d $HOME/.local \ - && mv $HOME/.local/bin/protoc /usr/local/bin/protoc \ - && mv $HOME/.local/include/ /usr/local/bin/include/ \ - && protoc --version \ # Install golangci-lint && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.49.0 \ # diff --git a/.dockerignore b/.dockerignore index ab088f702..b5695a33b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,16 +1,14 @@ -/target +*.md +*.yaml +*.yml +.* +/LICENSE /bin -.git -.gitignore /charts -/.vscode -/bin /cli -/examples +/config /docs -/.envrc -/.github -/README.md -/RELEASE_PROCESS.md -CONTRIBUTING.md +/examples +/target +/tests Dockerfile diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c7351348a..70df4d17c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -53,6 +53,9 @@ jobs: - name: Manifests run: make verify-manifests + - name: Mockgen + run: make verify-mockgen + - name: Build run: ARCH=${{ matrix.name }} make build @@ -71,5 +74,5 @@ jobs: with: go-version: 1.19 - name: Get golangci - run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.49.0 + run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.52.2 - uses: pre-commit/action@v3.0.0 diff --git a/.golangci.yml b/.golangci.yml index 9887b1b75..421b8c888 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -52,6 +52,14 @@ issues: - gomnd - dupl - unparam + # Exclude gci check for //+kubebuilder:scaffold:imports comments. Waiting to + # resolve https://github.com/kedacore/keda/issues/4379 + - path: operator/controllers/http/suite_test.go + linters: + - gci + - path: operator/main.go + linters: + - gci linters-settings: funlen: lines: 80 diff --git a/CHANGELOG.md b/CHANGELOG.md index 84cf2279a..a5d7ab50e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ This changelog keeps track of work items that have been completed and are ready - **General**: Automatically tag Docker image with commit SHA ([#567](https://github.com/kedacore/http-add-on/issues/567)) - **RBAC**: Introduce fine-grained permissions per component and reduce required permissions ([#612](https://github.com/kedacore/http-add-on/issues/612)) - **Operator**: Migrate project to Kubebuilder v3 ([#625](https://github.com/kedacore/http-add-on/issues/625)) +- **Routing**: New routing table implementation that relies on the live state of HTTPScaledObjects on the K8s Cluster instead of a ConfigMap that is updated periodically ([#605](https://github.com/kedacore/http-add-on/issues/605)) ### Fixes diff --git a/Makefile b/Makefile index 86dc90bc1..f19f420c9 100644 --- a/Makefile +++ b/Makefile @@ -34,14 +34,14 @@ GIT_COMMIT_SHORT ?= $(shell git rev-parse --short HEAD) # Build targets -build-operator: proto-gen - ${GO_BUILD_VARS} go build -ldflags $(GO_LDFLAGS) -a -o bin/operator ./operator +build-operator: + ${GO_BUILD_VARS} go build -ldflags $(GO_LDFLAGS) -trimpath -a -o bin/operator ./operator -build-interceptor: proto-gen - ${GO_BUILD_VARS} go build -ldflags $(GO_LDFLAGS) -a -o bin/interceptor ./interceptor +build-interceptor: + ${GO_BUILD_VARS} go build -ldflags $(GO_LDFLAGS) -trimpath -a -o bin/interceptor ./interceptor -build-scaler: proto-gen - ${GO_BUILD_VARS} go build -ldflags $(GO_LDFLAGS) -a -o bin/scaler ./scaler +build-scaler: + ${GO_BUILD_VARS} go build -ldflags $(GO_LDFLAGS) -trimpath -a -o bin/scaler ./scaler build: build-operator build-interceptor build-scaler @@ -85,9 +85,9 @@ publish-multiarch: publish-operator-multiarch publish-interceptor-multiarch publ # Development -generate: codegen manifests ## Generate code and manifests. +generate: codegen manifests mockgen ## Generate code, manifests, and mocks. -verify: verify-codegen verify-manifests ## Verify code and manifests. +verify: verify-codegen verify-manifests verify-mockgen ## Verify code, manifests, and mocks. codegen: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. $(CONTROLLER_GEN) object:headerFile='hack/boilerplate.go.txt' paths='./...' @@ -104,18 +104,24 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust verify-manifests: ## Verify manifests are up to date. ./hack/verify-manifests.sh +mockgen: ## Generate mock implementations of Go interfaces. + ./hack/update-mockgen.sh + +verify-mockgen: ## Verify mocks are up to date. + ./hack/verify-mockgen.sh + fmt: ## Run go fmt against code. go fmt ./... vet: ## Run go vet against code. go vet ./... +lint: ## Run golangci-lint against code. + golangci-lint run + pre-commit: ## Run static-checks. pre-commit run --all-files -proto-gen: protoc-gen-go ## Scaler protobuffers - protoc --proto_path=proto scaler.proto --go_out=proto --go-grpc_out=proto - CONTROLLER_GEN = $(shell pwd)/bin/controller-gen controller-gen: ## Download controller-gen locally if necessary. GOBIN=$(shell pwd)/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.12.0 @@ -124,10 +130,6 @@ KUSTOMIZE = $(shell pwd)/bin/kustomize kustomize: ## Download kustomize locally if necessary. GOBIN=$(shell pwd)/bin go install sigs.k8s.io/kustomize/kustomize/v5@v5.0.3 -protoc-gen-go: ## Download protoc-gen-go - go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1 - go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0 - deploy: manifests kustomize ## Deploy to the K8s cluster specified in ~/.kube/config. cd config/interceptor && \ $(KUSTOMIZE) edit set image ghcr.io/kedacore/http-add-on-interceptor=${IMAGE_INTERCEPTOR_VERSIONED_TAG} @@ -142,3 +144,8 @@ deploy: manifests kustomize ## Deploy to the K8s cluster specified in ~/.kube/co undeploy: $(KUSTOMIZE) build config/default | kubectl delete -f - + +kind-load: + kind load docker-image ghcr.io/kedacore/http-add-on-operator:${VERSION} + kind load docker-image ghcr.io/kedacore/http-add-on-interceptor:${VERSION} + kind load docker-image ghcr.io/kedacore/http-add-on-scaler:${VERSION} diff --git a/config/crd/bases/http.keda.sh_httpscaledobjects.yaml b/config/crd/bases/http.keda.sh_httpscaledobjects.yaml index e07eca884..7b6174c41 100644 --- a/config/crd/bases/http.keda.sh_httpscaledobjects.yaml +++ b/config/crd/bases/http.keda.sh_httpscaledobjects.yaml @@ -88,7 +88,7 @@ spec: type: object scaleTargetRef: description: The name of the deployment to route HTTP requests to - (and to autoscale). Either this or Image must be set + (and to autoscale). properties: deployment: description: The name of the deployment to scale according to diff --git a/config/interceptor/admin.service.yaml b/config/interceptor/admin.service.yaml index 6c8bc0c33..9f8007106 100644 --- a/config/interceptor/admin.service.yaml +++ b/config/interceptor/admin.service.yaml @@ -1,4 +1,3 @@ -# TODO(pedrotorres): remove after implementing new routing table apiVersion: v1 kind: Service metadata: diff --git a/config/interceptor/deployment.yaml b/config/interceptor/deployment.yaml index bf0fa2bd7..447233a91 100644 --- a/config/interceptor/deployment.yaml +++ b/config/interceptor/deployment.yaml @@ -53,11 +53,18 @@ spec: - name: KEDA_HTTP_EXPECT_CONTINUE_TIMEOUT value: "1s" ports: - # TODO(pedrotorres): remove after implementing new routing table - name: admin containerPort: 9090 - name: proxy containerPort: 8080 + livenessProbe: + httpGet: + path: /livez + port: proxy + readinessProbe: + httpGet: + path: /readyz + port: proxy # TODO(pedrotorres): set better default values avoiding overcommitment resources: requests: @@ -69,11 +76,12 @@ spec: securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true - runAsNonRoot: true capabilities: drop: - - ALL - seccompProfile: - type: RuntimeDefault + - ALL + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault serviceAccountName: interceptor terminationGracePeriodSeconds: 10 diff --git a/config/interceptor/role.yaml b/config/interceptor/role.yaml index 90ddf5fa0..b8d3428a2 100644 --- a/config/interceptor/role.yaml +++ b/config/interceptor/role.yaml @@ -12,17 +12,10 @@ rules: - get - list - watch ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: interceptor - namespace: keda -rules: - apiGroups: - - "" + - http.keda.sh resources: - - configmaps + - httpscaledobjects verbs: - get - list diff --git a/config/interceptor/role_binding.yaml b/config/interceptor/role_binding.yaml index 25459fcf0..fbed8bb04 100644 --- a/config/interceptor/role_binding.yaml +++ b/config/interceptor/role_binding.yaml @@ -10,15 +10,3 @@ roleRef: subjects: - kind: ServiceAccount name: interceptor ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: interceptor -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: interceptor -subjects: -- kind: ServiceAccount - name: interceptor diff --git a/config/operator/deployment.yaml b/config/operator/deployment.yaml index e84fcaf08..947cccd9b 100644 --- a/config/operator/deployment.yaml +++ b/config/operator/deployment.yaml @@ -27,25 +27,28 @@ spec: # TODO(pedrotorres): remove after implementing new routing table - --admin-port=9090 env: - # TODO(pedrotorres): remove after implementing new routing table - - name: KEDAHTTP_INTERCEPTOR_SERVICE - value: "keda-http-add-on-interceptor-admin" - name: KEDAHTTP_OPERATOR_EXTERNAL_SCALER_SERVICE value: "keda-http-add-on-external-scaler" - name: KEDAHTTP_OPERATOR_EXTERNAL_SCALER_PORT value: "9090" - - name: KEDAHTTP_INTERCEPTOR_ADMIN_PORT - value: "9090" - - name: KEDAHTTP_INTERCEPTOR_PROXY_PORT - value: "8080" - name: KEDA_HTTP_OPERATOR_NAMESPACE value: "keda" - name: KEDA_HTTP_OPERATOR_WATCH_NAMESPACE value: "" # TODO(pedrotorres): remove after implementing new routing table ports: - - name: admin - containerPort: 9090 + - name: metrics + containerPort: 8080 + - name: probes + containerPort: 8081 + livenessProbe: + httpGet: + path: /healthz + port: probes + readinessProbe: + httpGet: + path: /readyz + port: probes # TODO(pedrotorres): set better default values avoiding overcommitment resources: requests: @@ -57,11 +60,12 @@ spec: securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true - runAsNonRoot: true capabilities: drop: - - ALL - seccompProfile: - type: RuntimeDefault + - ALL + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault serviceAccountName: operator terminationGracePeriodSeconds: 10 diff --git a/config/scaler/deployment.yaml b/config/scaler/deployment.yaml index abacafc10..73d4718aa 100644 --- a/config/scaler/deployment.yaml +++ b/config/scaler/deployment.yaml @@ -23,13 +23,10 @@ spec: - name: scaler image: ghcr.io/kedacore/http-add-on-scaler env: - # TODO(pedrotorres): remove after implementing new routing table - name: KEDA_HTTP_SCALER_TARGET_ADMIN_DEPLOYMENT value: "keda-http-add-on-interceptor" - name: KEDA_HTTP_SCALER_PORT value: "9090" - - name: KEDA_HTTP_HEALTH_PORT - value: "9091" - name: KEDA_HTTP_SCALER_TARGET_ADMIN_NAMESPACE value: "keda" # TODO(pedrotorres): remove after implementing new routing table @@ -43,8 +40,14 @@ spec: ports: - name: grpc containerPort: 9090 - - name: health - containerPort: 9091 + livenessProbe: + grpc: + port: 9090 + service: liveness + readinessProbe: + grpc: + port: 9090 + service: readiness # TODO(pedrotorres): set better default values avoiding overcommitment resources: requests: @@ -56,11 +59,12 @@ spec: securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true - runAsNonRoot: true capabilities: drop: - ALL - seccompProfile: - type: RuntimeDefault + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault serviceAccountName: scaler terminationGracePeriodSeconds: 10 diff --git a/config/scaler/role.yaml b/config/scaler/role.yaml index f688dd686..124645503 100644 --- a/config/scaler/role.yaml +++ b/config/scaler/role.yaml @@ -20,6 +20,14 @@ rules: - get - list - watch +- apiGroups: + - http.keda.sh + resources: + - httpscaledobjects + verbs: + - get + - list + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role diff --git a/config/scaler/service.yaml b/config/scaler/service.yaml index 0e6ad791e..2d98f837c 100644 --- a/config/scaler/service.yaml +++ b/config/scaler/service.yaml @@ -8,7 +8,3 @@ spec: protocol: TCP port: 9090 targetPort: grpc - - name: health - protocol: TCP - port: 9091 - targetPort: health diff --git a/go.mod b/go.mod index 51a910be4..adaf807ce 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module github.com/kedacore/http-add-on go 1.19 require ( - github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe github.com/go-logr/logr v1.2.4 github.com/go-logr/zapr v1.2.3 - github.com/kedacore/keda/v2 v2.10.0 + github.com/golang/mock v1.7.0-rc.1.0.20220812172401-5b455625bd2c + github.com/hashicorp/go-immutable-radix/v2 v2.0.0 + github.com/kedacore/keda/v2 v2.10.1-0.20230601160236-b5de66fe3857 github.com/kelseyhightower/envconfig v1.4.0 github.com/onsi/ginkgo/v2 v2.9.4 github.com/onsi/gomega v1.27.6 @@ -15,20 +16,19 @@ require ( github.com/tj/assert v0.0.3 go.uber.org/zap v1.24.0 golang.org/x/sync v0.2.0 - google.golang.org/grpc v1.53.0 + google.golang.org/grpc v1.54.0 google.golang.org/protobuf v1.30.0 - k8s.io/api v0.26.3 - k8s.io/apimachinery v0.26.3 - k8s.io/client-go v0.26.3 - k8s.io/code-generator v0.26.3 - k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 - sigs.k8s.io/controller-runtime v0.14.5 + k8s.io/api v0.27.1 + k8s.io/apimachinery v0.27.1 + k8s.io/client-go v0.27.1 + k8s.io/code-generator v0.27.1 + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 + sigs.k8s.io/controller-runtime v0.15.0-alpha.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.10.2 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect @@ -46,8 +46,8 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect github.com/google/uuid v1.3.0 // indirect - github.com/imdario/mergo v0.3.14 // indirect - github.com/joho/godotenv v1.5.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.0 // indirect + github.com/imdario/mergo v0.3.15 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -57,17 +57,16 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_golang v1.15.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - go.uber.org/atomic v1.10.0 // indirect - go.uber.org/goleak v1.2.1 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.9.0 // indirect - golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sys v0.7.0 // indirect golang.org/x/term v0.7.0 // indirect golang.org/x/text v0.9.0 // indirect @@ -75,16 +74,16 @@ require ( golang.org/x/tools v0.8.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230320173215-1fe4d14fc725 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.26.3 // indirect - k8s.io/component-base v0.26.3 // indirect + k8s.io/apiextensions-apiserver v0.27.1 // indirect + k8s.io/component-base v0.27.1 // indirect k8s.io/gengo v0.0.0-20230306165830-ab3349d207d4 // indirect k8s.io/klog/v2 v2.90.1 // indirect - k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect - knative.dev/pkg v0.0.0-20230320014357-4c84b1b51ee8 // indirect + k8s.io/kube-openapi v0.0.0-20230426210814-b0c0aaee3cc0 // indirect + knative.dev/pkg v0.0.0-20230404101938-ee73c9355c9d // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/go.sum b/go.sum index 6ba38a720..7fca44160 100644 --- a/go.sum +++ b/go.sum @@ -17,16 +17,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= -github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe h1:69JI97HlzP+PH5Mi1thcGlDoBr6PS2Oe+l3mNmAkbs4= -github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE= github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -65,6 +60,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.7.0-rc.1.0.20220812172401-5b455625bd2c h1:8AzxBXzXPCzl8EEsgWirPPDA5ru+bm5dVEV/KkpAKnE= +github.com/golang/mock v1.7.0-rc.1.0.20220812172401-5b455625bd2c/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -103,25 +100,28 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo= -github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/hashicorp/go-immutable-radix/v2 v2.0.0 h1:nq9lQ5I71Heg2lRb2/+szuIWKY3Y73d8YKyXyN91WzU= +github.com/hashicorp/go-immutable-radix/v2 v2.0.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/golang-lru/v2 v2.0.0 h1:Lf+9eD8m5pncvHAOCQj49GSN6aQI8XGfI5OpXNkoWaA= +github.com/hashicorp/golang-lru/v2 v2.0.0/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kedacore/keda/v2 v2.10.0 h1:LOn+M/VVhBaNuhnqbZToZaSXG+JixYtNV5GLqqU77fk= -github.com/kedacore/keda/v2 v2.10.0/go.mod h1:iaJY4+Ukpg4yv/+B+UXxh+TY01umJFWh2BzH1lnlfXg= +github.com/kedacore/keda/v2 v2.10.1-0.20230601160236-b5de66fe3857 h1:Mvl+gPlyvf0mGp0ufBD0y1bqdVhQKr4M5LzDl4NQe5E= +github.com/kedacore/keda/v2 v2.10.1-0.20230601160236-b5de66fe3857/go.mod h1:xaQBtg5rfDDnv6OKwlJLCK8UCsU95OqxDH70XoPp19k= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -148,8 +148,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= @@ -158,6 +158,7 @@ github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -182,13 +183,13 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -199,12 +200,14 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20221215174704-0915cd710c24 h1:6w3iSY8IIkp5OQtbYj8NeuKG1jS9d+kYaubXqsoOiQ8= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -220,18 +223,20 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -243,6 +248,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -254,6 +260,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -268,6 +275,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -285,8 +293,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230320173215-1fe4d14fc725 h1:y5xTXcdn6niVQVAx56k92d+ybxvKjwGozMZ0jVnD38s= -google.golang.org/genproto v0.0.0-20230320173215-1fe4d14fc725/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -294,8 +302,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -330,31 +338,31 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.26.3 h1:emf74GIQMTik01Aum9dPP0gAypL8JTLl/lHa4V9RFSU= -k8s.io/api v0.26.3/go.mod h1:PXsqwPMXBSBcL1lJ9CYDKy7kIReUydukS5JiRlxC3qE= -k8s.io/apiextensions-apiserver v0.26.3 h1:5PGMm3oEzdB1W/FTMgGIDmm100vn7IaUP5er36dB+YE= -k8s.io/apiextensions-apiserver v0.26.3/go.mod h1:jdA5MdjNWGP+njw1EKMZc64xAT5fIhN6VJrElV3sfpQ= -k8s.io/apimachinery v0.26.3 h1:dQx6PNETJ7nODU3XPtrwkfuubs6w7sX0M8n61zHIV/k= -k8s.io/apimachinery v0.26.3/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= -k8s.io/client-go v0.26.3 h1:k1UY+KXfkxV2ScEL3gilKcF7761xkYsSD6BC9szIu8s= -k8s.io/client-go v0.26.3/go.mod h1:ZPNu9lm8/dbRIPAgteN30RSXea6vrCpFvq+MateTUuQ= -k8s.io/code-generator v0.26.3 h1:DNYPsWoeFwmg4qFg97Z1cHSSv7KSG10mAEIFoZGTQM8= -k8s.io/code-generator v0.26.3/go.mod h1:ryaiIKwfxEJEaywEzx3dhWOydpVctKYbqLajJf0O8dI= -k8s.io/component-base v0.26.3 h1:oC0WMK/ggcbGDTkdcqefI4wIZRYdK3JySx9/HADpV0g= -k8s.io/component-base v0.26.3/go.mod h1:5kj1kZYwSC6ZstHJN7oHBqcJC6yyn41eR+Sqa/mQc8E= +k8s.io/api v0.27.1 h1:Z6zUGQ1Vd10tJ+gHcNNNgkV5emCyW+v2XTmn+CLjSd0= +k8s.io/api v0.27.1/go.mod h1:z5g/BpAiD+f6AArpqNjkY+cji8ueZDU/WV1jcj5Jk4E= +k8s.io/apiextensions-apiserver v0.27.1 h1:Hp7B3KxKHBZ/FxmVFVpaDiXI6CCSr49P1OJjxKO6o4g= +k8s.io/apiextensions-apiserver v0.27.1/go.mod h1:8jEvRDtKjVtWmdkhOqE84EcNWJt/uwF8PC4627UZghY= +k8s.io/apimachinery v0.27.1 h1:EGuZiLI95UQQcClhanryclaQE6xjg1Bts6/L3cD7zyc= +k8s.io/apimachinery v0.27.1/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM= +k8s.io/client-go v0.27.1 h1:oXsfhW/qncM1wDmWBIuDzRHNS2tLhK3BZv512Nc59W8= +k8s.io/client-go v0.27.1/go.mod h1:f8LHMUkVb3b9N8bWturc+EDtVVVwZ7ueTVquFAJb2vA= +k8s.io/code-generator v0.27.1 h1:GrfUeUrJ/RtPskIsnChcXOW6h0EGNqty0VxxQ9qYKlM= +k8s.io/code-generator v0.27.1/go.mod h1:iWtpm0ZMG6Gc4daWfITDSIu+WFhFJArYDhj242zcbnY= +k8s.io/component-base v0.27.1 h1:kEB8p8lzi4gCs5f2SPU242vOumHJ6EOsOnDM3tTuDTM= +k8s.io/component-base v0.27.1/go.mod h1:UGEd8+gxE4YWoigz5/lb3af3Q24w98pDseXcXZjw+E0= k8s.io/gengo v0.0.0-20230306165830-ab3349d207d4 h1:aClvVG6GbX10ISHcc24J+tqbr0S7fEe1MWkFJ7cWWCI= k8s.io/gengo v0.0.0-20230306165830-ab3349d207d4/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg= -k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY= -k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 h1:xMMXJlJbsU8w3V5N2FLDQ8YgU8s1EoULdbQBcAeNJkY= -k8s.io/utils v0.0.0-20230313181309-38a27ef9d749/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -knative.dev/pkg v0.0.0-20230320014357-4c84b1b51ee8 h1:bkOWi8rrtMWtkDJLnWFw6w9iuqDBE/4RRb5pTtuYvjQ= -knative.dev/pkg v0.0.0-20230320014357-4c84b1b51ee8/go.mod h1:S+KfTInuwEkZSTwvWqrWZV/TEw6ps51GUGaSC1Fnbe0= -sigs.k8s.io/controller-runtime v0.14.5 h1:6xaWFqzT5KuAQ9ufgUaj1G/+C4Y1GRkhrxl+BJ9i+5s= -sigs.k8s.io/controller-runtime v0.14.5/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= +k8s.io/kube-openapi v0.0.0-20230426210814-b0c0aaee3cc0 h1:XET+pmtvzC9NYUnHIX8PUPDoxqMTtDCJMRfJpoUSWow= +k8s.io/kube-openapi v0.0.0-20230426210814-b0c0aaee3cc0/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +knative.dev/pkg v0.0.0-20230404101938-ee73c9355c9d h1:mubqXUjYfnwNg3IGWYEj2YffXYIxg44Qn9GS5vPAjck= +knative.dev/pkg v0.0.0-20230404101938-ee73c9355c9d/go.mod h1:EQk8+qkZ8fMtrDYOOb9e9xMQG29N+L54iXBCfNXRm90= +sigs.k8s.io/controller-runtime v0.15.0-alpha.0 h1:ukmgReObs7FEUNBcn2NLxn/DiEQ8g1yC8YvpX0HGiyE= +sigs.k8s.io/controller-runtime v0.15.0-alpha.0/go.mod h1:icJQ1mtZAutJ9iOzS2V2VJQCBVV2ir+xahBeTHCCZGs= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= diff --git a/hack/tools.go b/hack/tools.go index 596af0aa4..3c8ea465f 100644 --- a/hack/tools.go +++ b/hack/tools.go @@ -22,5 +22,6 @@ limitations under the License. package hack import ( + _ "github.com/golang/mock/mockgen" _ "k8s.io/code-generator" ) diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 21e5d8e6f..db68221ae 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -24,8 +24,8 @@ SCRIPT_ROOT="$(dirname "${BASH_SOURCE[0]}")/.." OUTPUT_BASE="$(mktemp -d)" GO_PACKAGE='github.com/kedacore/http-add-on' -GEN_SUFFIX="operator/generated" -API_SUFFIX="operator/apis" +GEN_SUFFIX='operator/generated' +API_SUFFIX='operator/apis' . "${CODEGEN_PKG}/generate-groups.sh" \ 'client,informer,lister' \ diff --git a/hack/update-mockgen.sh b/hack/update-mockgen.sh new file mode 100755 index 000000000..52a2a8df2 --- /dev/null +++ b/hack/update-mockgen.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Copyright 2017 The Kubernetes Authors. +# Copyright 2023 The KEDA Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +OUTPUT="$(mktemp -d)" + +GEN='operator/generated' +CPY='hack/boilerplate.go.txt' +PKG='mock' + +MOCKGEN_PKG="${MOCKGEN_PKG:-$(go list -f '{{ .Dir }}' -m github.com/golang/mock 2>/dev/null)/mockgen}" +MOCKGEN="${OUTPUT}/mockgen" +go build -o "${MOCKGEN}" "${MOCKGEN_PKG}" + +for SRC in $(find "${GEN}" -type 'f' -name '*.go' | grep -v '/fake/' | grep -v "/${PKG}/") +do + DIR="$(dirname "${SRC}")/${PKG}" + mkdir -p "${DIR}" + DST="${DIR}/$(basename "${SRC}")" + "${MOCKGEN}" -copyright_file="${CPY}" -destination="${DST}" -package="${PKG}" -source="${SRC}" +done + +rm -fR "${OUTPUT}" diff --git a/hack/verify-mockgen.sh b/hack/verify-mockgen.sh new file mode 100755 index 000000000..06c610d8e --- /dev/null +++ b/hack/verify-mockgen.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# Copyright 2017 The Kubernetes Authors. +# Copyright 2023 The KEDA Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. + +DIFFROOT="${SCRIPT_ROOT}/operator" +TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/operator" +_tmp="${SCRIPT_ROOT}/_tmp" + +cleanup() { + rm -rf "${_tmp}" +} +trap "cleanup" EXIT SIGINT + +cleanup + +mkdir -p "${TMP_DIFFROOT}" +cp -a "${DIFFROOT}"/. "${TMP_DIFFROOT}" + +make mockgen +echo "diffing ${DIFFROOT} against freshly generated mockgen" +ret=0 +diff -Napru "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$? +cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}" +if [[ $ret -eq 0 ]] +then + echo "${DIFFROOT} up to date." +else + echo "${DIFFROOT} is out of date. Please run '${SCRIPT_ROOT}/hack/update-mockgen.sh'" + exit 1 +fi diff --git a/interceptor/Dockerfile b/interceptor/Dockerfile index 0a99a3f10..89bcd5a8d 100644 --- a/interceptor/Dockerfile +++ b/interceptor/Dockerfile @@ -1,29 +1,14 @@ -# Build the adapter binary -FROM --platform=$BUILDPLATFORM ghcr.io/kedacore/build-tools:1.19.5 as builder - -ARG VERSION=main -ARG GIT_COMMIT=HEAD - +FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/build-tools:1.20.4 as builder WORKDIR /workspace - -COPY go.mod go.mod -COPY go.sum go.sum +COPY go.* . RUN go mod download - COPY . . - -# Build -# https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/ +ARG VERSION=main +ARG GIT_COMMIT=HEAD ARG TARGETOS ARG TARGETARCH -RUN VERSION=${VERSION} GIT_COMMIT=${GIT_COMMIT} TARGET_OS=$TARGETOS ARCH=$TARGETARCH make build-interceptor - +RUN VERSION="${VERSION}" GIT_COMMIT="${GIT_COMMIT}" TARGET_OS="${TARGETOS}" ARCH="${TARGETARCH}" make build-interceptor FROM gcr.io/distroless/static:nonroot -WORKDIR / -COPY --from=builder /workspace/bin/interceptor . -# 65532 is numeric for nonroot -USER 65532:65532 -EXPOSE 8080 - -ENTRYPOINT ["/interceptor"] +COPY --from=builder /workspace/bin/interceptor /sbin/init +ENTRYPOINT ["/sbin/init"] diff --git a/interceptor/config/serving.go b/interceptor/config/serving.go index 10068ef44..a3ff5879e 100644 --- a/interceptor/config/serving.go +++ b/interceptor/config/serving.go @@ -12,6 +12,9 @@ type Serving struct { // CurrentNamespace is the namespace that the interceptor is // currently running in CurrentNamespace string `envconfig:"KEDA_HTTP_CURRENT_NAMESPACE" required:"true"` + // WatchNamespace is the namespace to watch for new HTTPScaledObjects. + // Leave this empty to watch HTTPScaledObjects in all namespaces. + WatchNamespace string `envconfig:"KEDA_HTTP_WATCH_NAMESPACE" default:""` // ProxyPort is the port that the public proxy should run on ProxyPort int `envconfig:"KEDA_HTTP_PROXY_PORT" required:"true"` // AdminPort is the port that the internal admin server should run on. diff --git a/interceptor/forward_wait_func_test.go b/interceptor/forward_wait_func_test.go index 96be8be8b..652dea649 100644 --- a/interceptor/forward_wait_func_test.go +++ b/interceptor/forward_wait_func_test.go @@ -8,7 +8,9 @@ import ( "github.com/go-logr/logr" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/watch" "github.com/kedacore/http-add-on/pkg/k8s" @@ -127,3 +129,61 @@ func TestWaitFuncWaitsUntilReplicas(t *testing.T) { r.NoError(err) done() } + +// newDeployment creates a new deployment object +// with the given name and the given image. This does not actually create +// the deployment in the cluster, it just creates the deployment object +// in memory +func newDeployment( + namespace, + name, + image string, + ports []int32, + env []corev1.EnvVar, + labels map[string]string, + pullPolicy corev1.PullPolicy, +) *appsv1.Deployment { + containerPorts := make([]corev1.ContainerPort, len(ports)) + for i, port := range ports { + containerPorts[i] = corev1.ContainerPort{ + ContainerPort: port, + } + } + deployment := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Replicas: k8s.Int32P(1), + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: image, + Name: name, + ImagePullPolicy: pullPolicy, + Ports: containerPorts, + Env: env, + }, + }, + }, + }, + }, + Status: appsv1.DeploymentStatus{ + ReadyReplicas: 1, + }, + } + + return deployment +} diff --git a/interceptor/handler/probe.go b/interceptor/handler/probe.go new file mode 100644 index 000000000..9be78eaa9 --- /dev/null +++ b/interceptor/handler/probe.go @@ -0,0 +1,69 @@ +package handler + +import ( + "context" + "net/http" + "sync/atomic" + "time" + + "github.com/kedacore/http-add-on/pkg/util" +) + +type Probe struct { + healthCheckers []util.HealthChecker + healthy atomic.Bool +} + +func NewProbe(healthChecks []util.HealthChecker) *Probe { + return &Probe{ + healthCheckers: healthChecks, + } +} + +var _ http.Handler = (*Probe)(nil) + +func (ph *Probe) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r = util.RequestWithLoggerWithName(r, "ProbeHandler") + ctx := r.Context() + + sc := http.StatusOK + if !ph.healthy.Load() { + sc = http.StatusServiceUnavailable + } + w.WriteHeader(sc) + + st := http.StatusText(sc) + if _, err := w.Write([]byte(st)); err != nil { + logger := util.LoggerFromContext(ctx) + logger.Error(err, "write failed") + } +} + +func (ph *Probe) Start(ctx context.Context) { + for { + ph.check(ctx) + + select { + case <-ctx.Done(): + return + case <-time.After(time.Second): + continue + } + } +} + +func (ph *Probe) check(ctx context.Context) { + logger := util.LoggerFromContext(ctx) + logger = logger.WithName("Probe") + + for _, hc := range ph.healthCheckers { + if err := hc.HealthCheck(ctx); err != nil { + ph.healthy.Store(false) + + logger.Error(err, "health check function failed") + return + } + } + + ph.healthy.Store(true) +} diff --git a/interceptor/handler/probe_test.go b/interceptor/handler/probe_test.go new file mode 100644 index 000000000..41cb716c7 --- /dev/null +++ b/interceptor/handler/probe_test.go @@ -0,0 +1,250 @@ +package handler + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "time" + + "github.com/go-logr/logr/funcr" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/kedacore/http-add-on/pkg/util" +) + +var _ = Describe("ProbeHandler", func() { + Context("New", func() { + It("returns new object with expected fields", func() { + var ( + ctx = context.Background() + ret = errors.New("test error") + ) + + var b bool + healthCheckers := []util.HealthChecker{ + util.HealthCheckerFunc(func(_ context.Context) error { + b = true + + return ret + }), + } + + ph := NewProbe(healthCheckers) + Expect(ph).NotTo(BeNil()) + + h := ph.healthy.Load() + Expect(h).To(BeFalse()) + + hcs := ph.healthCheckers + Expect(hcs).To(HaveLen(1)) + + hc := hcs[0] + Expect(hc).NotTo(BeNil()) + + err := hc.HealthCheck(ctx) + Expect(err).To(MatchError(ret)) + + Expect(b).To(BeTrue()) + }) + }) + + Context("ServeHTTP", func() { + const ( + host = "keda.sh" + path = "/README" + ) + + var ( + w *httptest.ResponseRecorder + r *http.Request + ) + + BeforeEach(func() { + w = httptest.NewRecorder() + + r = httptest.NewRequest(http.MethodGet, path, nil) + r.Host = host + }) + + When("healthy", func() { + It("serves 200", func() { + var ( + sc = http.StatusOK + st = http.StatusText(sc) + ) + + var ph Probe + ph.healthy.Store(true) + + ph.ServeHTTP(w, r) + + Expect(w.Code).To(Equal(sc)) + Expect(w.Body.String()).To(Equal(st)) + }) + }) + + When("unhealthy", func() { + It("serves 503", func() { + var ( + sc = http.StatusServiceUnavailable + st = http.StatusText(sc) + ) + + var ph Probe + ph.healthy.Store(false) + + ph.ServeHTTP(w, r) + + Expect(w.Code).To(Equal(sc)) + Expect(w.Body.String()).To(Equal(st)) + }) + }) + }) + + Context("Start", func() { + It("returns when context is done", func() { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + var ph Probe + err := util.WithTimeout(time.Second, func() error { + ph.Start(ctx) + + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("invokes check every second", func() { + const ( + n = 10 + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var i int + ph := Probe{ + healthCheckers: []util.HealthChecker{ + util.HealthCheckerFunc(func(_ context.Context) error { + i++ + + return nil + }), + }, + } + + go ph.Start(ctx) + + time.Sleep(n * time.Second) + + Expect(i).To(BeNumerically("~", n, 2)) + }) + }) + + Context("check", func() { + When("all health checks succeed", func() { + It("sets healthy to true", func() { + var ( + ctx = context.Background() + ) + + var bs []bool + ph := Probe{ + healthCheckers: []util.HealthChecker{ + util.HealthCheckerFunc(func(_ context.Context) error { + bs = append(bs, true) + return nil + }), + util.HealthCheckerFunc(func(_ context.Context) error { + bs = append(bs, true) + return nil + }), + util.HealthCheckerFunc(func(_ context.Context) error { + bs = append(bs, true) + return nil + }), + }, + } + + ph.check(ctx) + + healthCheckersLen := len(ph.healthCheckers) + Expect(bs).To(HaveLen(healthCheckersLen)) + + healthy := ph.healthy.Load() + Expect(healthy).To(BeTrue()) + }) + }) + + When("a health check fail", func() { + It("sets healthy to false", func() { + var ( + ctx = context.Background() + ) + + ph := Probe{ + healthCheckers: []util.HealthChecker{ + util.HealthCheckerFunc(func(_ context.Context) error { + return nil + }), + util.HealthCheckerFunc(func(_ context.Context) error { + return context.Canceled + }), + util.HealthCheckerFunc(func(_ context.Context) error { + return nil + }), + }, + } + ph.healthy.Store(true) + + ph.check(ctx) + + healthy := ph.healthy.Load() + Expect(healthy).To(BeFalse()) + }) + + It("logs the returned error", func() { + const ( + msg = "health check function failed" + ) + + var ( + ctx = context.Background() + ret = errors.New("test error") + ) + + var b bool + ctx = util.ContextWithLogger(ctx, funcr.NewJSON( + func(obj string) { + var m map[string]interface{} + + err := json.Unmarshal([]byte(obj), &m) + Expect(err).NotTo(HaveOccurred()) + + Expect(m).To(HaveKeyWithValue("msg", msg)) + Expect(m).To(HaveKeyWithValue("error", ret.Error())) + + b = true + }, + funcr.Options{}, + )) + + ph := Probe{ + healthCheckers: []util.HealthChecker{ + util.HealthCheckerFunc(func(_ context.Context) error { + return ret + }), + }, + } + + ph.check(ctx) + + Expect(b).To(BeTrue()) + }) + }) + }) +}) diff --git a/interceptor/handler/static.go b/interceptor/handler/static.go new file mode 100644 index 000000000..54c77953a --- /dev/null +++ b/interceptor/handler/static.go @@ -0,0 +1,42 @@ +package handler + +import ( + "net/http" + + "github.com/kedacore/http-add-on/pkg/k8s" + "github.com/kedacore/http-add-on/pkg/routing" + "github.com/kedacore/http-add-on/pkg/util" +) + +type Static struct { + statusCode int + err error +} + +func NewStatic(statusCode int, err error) *Static { + return &Static{ + statusCode: statusCode, + err: err, + } +} + +var _ http.Handler = (*Static)(nil) + +func (sh *Static) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r = util.RequestWithLoggerWithName(r, "StaticHandler") + ctx := r.Context() + + logger := util.LoggerFromContext(ctx) + httpso := util.HTTPSOFromContext(ctx) + stream := util.StreamFromContext(ctx) + + statusText := http.StatusText(sh.statusCode) + routingKey := routing.NewKeyFromRequest(r) + namespacedName := k8s.NamespacedNameFromObject(httpso) + logger.Error(sh.err, statusText, "routingKey", routingKey, "namespacedName", namespacedName, "stream", stream) + + w.WriteHeader(sh.statusCode) + if _, err := w.Write([]byte(statusText)); err != nil { + logger.Error(err, "write failed") + } +} diff --git a/interceptor/handler/static_test.go b/interceptor/handler/static_test.go new file mode 100644 index 000000000..4cf4b9323 --- /dev/null +++ b/interceptor/handler/static_test.go @@ -0,0 +1,65 @@ +package handler + +import ( + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + + "github.com/go-logr/logr/funcr" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/kedacore/http-add-on/pkg/routing" + "github.com/kedacore/http-add-on/pkg/util" +) + +var _ = Describe("ServeHTTP", func() { + var ( + w *httptest.ResponseRecorder + r *http.Request + + sc = http.StatusTeapot + st = http.StatusText(sc) + + se = errors.New("test error") + ) + + BeforeEach(func() { + w = httptest.NewRecorder() + r = httptest.NewRequest(http.MethodGet, "/", nil) + }) + + It("serves expected status code and body", func() { + sh := NewStatic(sc, nil) + sh.ServeHTTP(w, r) + + Expect(w.Code).To(Equal(sc)) + Expect(w.Body.String()).To(Equal(st)) + }) + + It("logs the failed request", func() { + var b bool + r = r.WithContext(util.ContextWithLogger(r.Context(), funcr.NewJSON( + func(obj string) { + var m map[string]interface{} + + err := json.Unmarshal([]byte(obj), &m) + Expect(err).NotTo(HaveOccurred()) + + rk := routing.NewKeyFromRequest(r) + Expect(m).To(HaveKeyWithValue("error", se.Error())) + Expect(m).To(HaveKeyWithValue("msg", st)) + Expect(m).To(HaveKeyWithValue("routingKey", rk.String())) + + b = true + }, + funcr.Options{}, + ))) + + sh := NewStatic(sc, se) + sh.ServeHTTP(w, r) + + Expect(b).To(BeTrue()) + }) +}) diff --git a/interceptor/handler/suite_test.go b/interceptor/handler/suite_test.go new file mode 100644 index 000000000..8d8eb24d1 --- /dev/null +++ b/interceptor/handler/suite_test.go @@ -0,0 +1,14 @@ +package handler + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestHandler(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Handler Suite") +} diff --git a/interceptor/handler/upstream.go b/interceptor/handler/upstream.go new file mode 100644 index 000000000..c3658c6e8 --- /dev/null +++ b/interceptor/handler/upstream.go @@ -0,0 +1,56 @@ +package handler + +import ( + "errors" + "net/http" + "net/http/httputil" + + "github.com/kedacore/http-add-on/pkg/util" +) + +var ( + errNilStream = errors.New("context stream is nil") +) + +type Upstream struct { + roundTripper http.RoundTripper +} + +func NewUpstream(roundTripper http.RoundTripper) *Upstream { + return &Upstream{ + roundTripper: roundTripper, + } +} + +var _ http.Handler = (*Upstream)(nil) + +func (uh *Upstream) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r = util.RequestWithLoggerWithName(r, "UpstreamHandler") + ctx := r.Context() + + stream := util.StreamFromContext(ctx) + if stream == nil { + sh := NewStatic(http.StatusInternalServerError, errNilStream) + sh.ServeHTTP(w, r) + + return + } + + proxy := httputil.NewSingleHostReverseProxy(stream) + proxy.Transport = uh.roundTripper + proxy.Director = func(req *http.Request) { + req.URL = stream + req.Host = stream.Host + req.URL.Path = r.URL.Path + req.URL.RawQuery = r.URL.RawQuery + // delete the incoming X-Forwarded-For header so the proxy + // puts its own in. This is also important to prevent IP spoofing + req.Header.Del("X-Forwarded-For ") + } + proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + sh := NewStatic(http.StatusBadGateway, err) + sh.ServeHTTP(w, r) + } + + proxy.ServeHTTP(w, r) +} diff --git a/interceptor/request_forwarder_test.go b/interceptor/handler/upstream_test.go similarity index 84% rename from interceptor/request_forwarder_test.go rename to interceptor/handler/upstream_test.go index 51d749ae8..e7cfe290d 100644 --- a/interceptor/request_forwarder_test.go +++ b/interceptor/handler/upstream_test.go @@ -1,4 +1,4 @@ -package main +package handler import ( "log" @@ -8,57 +8,14 @@ import ( "testing" "time" - "github.com/go-logr/logr" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/wait" "github.com/kedacore/http-add-on/interceptor/config" kedanet "github.com/kedacore/http-add-on/pkg/net" + "github.com/kedacore/http-add-on/pkg/util" ) -func newRoundTripper( - dialCtxFunc kedanet.DialContextFunc, - httpRespHeaderTimeout time.Duration, -) http.RoundTripper { - return &http.Transport{ - DialContext: dialCtxFunc, - ResponseHeaderTimeout: httpRespHeaderTimeout, - } -} - -func defaultTimeouts() config.Timeouts { - return config.Timeouts{ - Connect: 100 * time.Millisecond, - KeepAlive: 100 * time.Millisecond, - ResponseHeader: 500 * time.Millisecond, - DeploymentReplicas: 1 * time.Second, - } -} - -// returns a kedanet.DialContextFunc by calling kedanet.DialContextWithRetry. if you pass nil for the -// timeoutConfig, it uses standard values. otherwise it uses the one you passed. -// -// the returned config.Timeouts is what was passed to the DialContextWithRetry function -func retryDialContextFunc( - timeouts config.Timeouts, - backoff wait.Backoff, -) kedanet.DialContextFunc { - dialer := kedanet.NewNetDialer( - timeouts.Connect, - timeouts.KeepAlive, - ) - return kedanet.DialContextWithRetry(dialer, backoff) -} - -func reqAndRes(path string) (*httptest.ResponseRecorder, *http.Request, error) { - req, err := http.NewRequest("GET", path, nil) - if err != nil { - return nil, nil, err - } - resRecorder := httptest.NewRecorder() - return resRecorder, req, nil -} - func TestForwarderSuccess(t *testing.T) { r := require.New(t) // this channel will be closed after the request was received, but @@ -82,15 +39,12 @@ func TestForwarderSuccess(t *testing.T) { const path = "/testfwd" res, req, err := reqAndRes(path) r.NoError(err) + req = util.RequestWithStream(req, forwardURL) timeouts := defaultTimeouts() dialCtxFunc := retryDialContextFunc(timeouts, timeouts.DefaultBackoff()) - forwardRequest( - logr.Discard(), - res, - req, - newRoundTripper(dialCtxFunc, timeouts.ResponseHeader), - forwardURL, - ) + rt := newRoundTripper(dialCtxFunc, timeouts.ResponseHeader) + uh := NewUpstream(rt) + uh.ServeHTTP(res, req) r.True( ensureSignalBeforeTimeout(reqRecvCh, 100*time.Millisecond), @@ -126,24 +80,21 @@ func TestForwarderHeaderTimeout(t *testing.T) { defer srv.Close() timeouts := defaultTimeouts() - timeouts.Connect = 10 * time.Millisecond - timeouts.ResponseHeader = 10 * time.Millisecond + timeouts.Connect = 1 * time.Millisecond + timeouts.ResponseHeader = 1 * time.Millisecond backoff := timeouts.Backoff(2, 2, 1) dialCtxFunc := retryDialContextFunc(timeouts, backoff) res, req, err := reqAndRes("/testfwd") r.NoError(err) - forwardRequest( - logr.Discard(), - res, - req, - newRoundTripper(dialCtxFunc, timeouts.ResponseHeader), - originURL, - ) + req = util.RequestWithStream(req, originURL) + rt := newRoundTripper(dialCtxFunc, timeouts.ResponseHeader) + uh := NewUpstream(rt) + uh.ServeHTTP(res, req) forwardedRequests := hdl.IncomingRequests() r.Equal(0, len(forwardedRequests)) r.Equal(502, res.Code) - r.Contains(res.Body.String(), "error on backend") + r.Contains(res.Body.String(), http.StatusText(http.StatusBadGateway)) // the proxy has bailed out, so tell the origin to stop close(originWaitCh) } @@ -185,13 +136,10 @@ func TestForwarderWaitsForSlowOrigin(t *testing.T) { const path = "/testfwd" res, req, err := reqAndRes(path) r.NoError(err) - forwardRequest( - logr.Discard(), - res, - req, - newRoundTripper(dialCtxFunc, timeouts.ResponseHeader), - originURL, - ) + req = util.RequestWithStream(req, originURL) + rt := newRoundTripper(dialCtxFunc, timeouts.ResponseHeader) + uh := NewUpstream(rt) + uh.ServeHTTP(res, req) // wait for the goroutine above to finish, with a little cusion ensureSignalBeforeTimeout(originWaitCh, originDelay*2) r.Equal(originRespCode, res.Code) @@ -211,15 +159,12 @@ func TestForwarderConnectionRetryAndTimeout(t *testing.T) { dialCtxFunc := retryDialContextFunc(timeouts, timeouts.DefaultBackoff()) res, req, err := reqAndRes("/test") r.NoError(err) + req = util.RequestWithStream(req, noSuchURL) + rt := newRoundTripper(dialCtxFunc, timeouts.ResponseHeader) + uh := NewUpstream(rt) start := time.Now() - forwardRequest( - logr.Discard(), - res, - req, - newRoundTripper(dialCtxFunc, timeouts.ResponseHeader), - noSuchURL, - ) + uh.ServeHTTP(res, req) elapsed := time.Since(start) log.Printf("forwardRequest took %s", elapsed) @@ -242,7 +187,7 @@ func TestForwarderConnectionRetryAndTimeout(t *testing.T) { "unexpected code (response body was '%s')", res.Body.String(), ) - r.Contains(res.Body.String(), "error on backend") + r.Contains(res.Body.String(), http.StatusText(http.StatusBadGateway)) } func TestForwardRequestRedirectAndHeaders(t *testing.T) { @@ -270,16 +215,69 @@ func TestForwardRequestRedirectAndHeaders(t *testing.T) { dialCtxFunc := retryDialContextFunc(timeouts, backoff) res, req, err := reqAndRes("/testfwd") r.NoError(err) - forwardRequest( - logr.Discard(), - res, - req, - newRoundTripper(dialCtxFunc, timeouts.ResponseHeader), - srvURL, - ) + req = util.RequestWithStream(req, srvURL) + rt := newRoundTripper(dialCtxFunc, timeouts.ResponseHeader) + uh := NewUpstream(rt) + uh.ServeHTTP(res, req) r.Equal(301, res.Code) r.Equal("abc123.com", res.Header().Get("Location")) r.Equal("text/html; charset=utf-8", res.Header().Get("Content-Type")) r.Equal("somethingcustom", res.Header().Get("X-Custom-Header")) r.Equal("Hello from srv", res.Body.String()) } + +func newRoundTripper( + dialCtxFunc kedanet.DialContextFunc, + httpRespHeaderTimeout time.Duration, +) http.RoundTripper { + return &http.Transport{ + DialContext: dialCtxFunc, + ResponseHeaderTimeout: httpRespHeaderTimeout, + } +} + +func defaultTimeouts() config.Timeouts { + return config.Timeouts{ + Connect: 100 * time.Millisecond, + KeepAlive: 100 * time.Millisecond, + ResponseHeader: 500 * time.Millisecond, + DeploymentReplicas: 1 * time.Second, + } +} + +// returns a kedanet.DialContextFunc by calling kedanet.DialContextWithRetry. if you pass nil for the +// timeoutConfig, it uses standard values. otherwise it uses the one you passed. +// +// the returned config.Timeouts is what was passed to the DialContextWithRetry function +func retryDialContextFunc( + timeouts config.Timeouts, + backoff wait.Backoff, +) kedanet.DialContextFunc { + dialer := kedanet.NewNetDialer( + timeouts.Connect, + timeouts.KeepAlive, + ) + return kedanet.DialContextWithRetry(dialer, backoff) +} + +func reqAndRes(path string) (*httptest.ResponseRecorder, *http.Request, error) { + req, err := http.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + resRecorder := httptest.NewRecorder() + return resRecorder, req, nil +} + +// ensureSignalAfter returns true if signalCh receives before timeout, false otherwise. +// it blocks for timeout at most +func ensureSignalBeforeTimeout(signalCh <-chan struct{}, timeout time.Duration) bool { + timer := time.NewTimer(timeout) + defer timer.Stop() + select { + case <-timer.C: + return false + case <-signalCh: + return true + } +} diff --git a/interceptor/k8s_objects_util_test.go b/interceptor/k8s_objects_util_test.go deleted file mode 100644 index a71c0f1db..000000000 --- a/interceptor/k8s_objects_util_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/kedacore/http-add-on/pkg/k8s" -) - -// newDeployment creates a new deployment object -// with the given name and the given image. This does not actually create -// the deployment in the cluster, it just creates the deployment object -// in memory -func newDeployment( - namespace, - name, - image string, - ports []int32, - env []corev1.EnvVar, - labels map[string]string, - pullPolicy corev1.PullPolicy, -) *appsv1.Deployment { - containerPorts := make([]corev1.ContainerPort, len(ports)) - for i, port := range ports { - containerPorts[i] = corev1.ContainerPort{ - ContainerPort: port, - } - } - deployment := &appsv1.Deployment{ - TypeMeta: metav1.TypeMeta{ - Kind: "Deployment", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: labels, - }, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: labels, - }, - Replicas: k8s.Int32P(1), - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Image: image, - Name: name, - ImagePullPolicy: pullPolicy, - Ports: containerPorts, - Env: env, - }, - }, - }, - }, - }, - Status: appsv1.DeploymentStatus{ - ReadyReplicas: 1, - }, - } - - return deployment -} diff --git a/interceptor/main.go b/interceptor/main.go index 2cfa56821..3314ee85d 100644 --- a/interceptor/main.go +++ b/interceptor/main.go @@ -2,19 +2,22 @@ package main import ( "context" - "encoding/json" "fmt" "math/rand" - nethttp "net/http" + "net/http" "os" "time" "github.com/go-logr/logr" "golang.org/x/sync/errgroup" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" "github.com/kedacore/http-add-on/interceptor/config" + "github.com/kedacore/http-add-on/interceptor/handler" + "github.com/kedacore/http-add-on/interceptor/middleware" + clientset "github.com/kedacore/http-add-on/operator/generated/clientset/versioned" + informers "github.com/kedacore/http-add-on/operator/generated/informers/externalversions" "github.com/kedacore/http-add-on/pkg/build" kedahttp "github.com/kedacore/http-add-on/pkg/http" "github.com/kedacore/http-add-on/pkg/k8s" @@ -22,14 +25,15 @@ import ( kedanet "github.com/kedacore/http-add-on/pkg/net" "github.com/kedacore/http-add-on/pkg/queue" "github.com/kedacore/http-add-on/pkg/routing" + "github.com/kedacore/http-add-on/pkg/util" ) func init() { rand.Seed(time.Now().UnixNano()) } -// +kubebuilder:rbac:groups="",namespace=keda,resources=configmaps,verbs=get;list;watch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch +// +kubebuilder:rbac:groups=http.keda.sh,resources=httpscaledobjects,verbs=get;list;watch func main() { lggr, err := pkglog.NewZapr() @@ -43,9 +47,8 @@ func main() { lggr.Error(err, "invalid configuration") os.Exit(1) } - ctx, ctxDone := context.WithCancel( - context.Background(), - ) + ctx := util.ContextWithLogger(context.Background(), lggr) + ctx, ctxDone := context.WithCancel(ctx) lggr.Info( "starting interceptor", "timeoutConfig", @@ -57,11 +60,8 @@ func main() { proxyPort := servingCfg.ProxyPort adminPort := servingCfg.AdminPort - cfg, err := rest.InClusterConfig() - if err != nil { - lggr.Error(err, "Kubernetes client config not found") - os.Exit(1) - } + cfg := ctrl.GetConfigOrDie() + cl, err := kubernetes.NewForConfig(cfg) if err != nil { lggr.Error(err, "creating new Kubernetes ClientSet") @@ -76,40 +76,24 @@ func main() { lggr.Error(err, "creating new deployment cache") os.Exit(1) } - - configMapsInterface := cl.CoreV1().ConfigMaps(servingCfg.CurrentNamespace) - waitFunc := newDeployReplicasForwardWaitFunc(lggr, deployCache) - lggr.Info("Interceptor starting") - - q := queue.NewMemory() - routingTable := routing.NewTable() - - // Create the informer of ConfigMap resource, - // the resynchronization period of the informer should be not less than 1s, - // refer to: https://github.com/kubernetes/client-go/blob/v0.22.2/tools/cache/shared_informer.go#L475 - configMapInformer := k8s.NewInformerConfigMapUpdater( - lggr, - cl, - servingCfg.ConfigMapCacheRsyncPeriod, - servingCfg.CurrentNamespace, - ) - - lggr.Info( - "Fetching initial routing table", - ) - if err := routing.GetTable( - ctx, - lggr, - configMapsInterface, - routingTable, - q, - ); err != nil { + httpCl, err := clientset.NewForConfig(cfg) + if err != nil { + lggr.Error(err, "creating new HTTP ClientSet") + os.Exit(1) + } + sharedInformerFactory := informers.NewSharedInformerFactory(httpCl, servingCfg.ConfigMapCacheRsyncPeriod) + routingTable, err := routing.NewTable(sharedInformerFactory, servingCfg.WatchNamespace) + if err != nil { lggr.Error(err, "fetching routing table") os.Exit(1) } + q := queue.NewMemory() + + lggr.Info("Interceptor starting") + errGrp, ctx := errgroup.WithContext(ctx) // start the deployment cache updater @@ -125,14 +109,7 @@ func main() { // enter and exit the system errGrp.Go(func() error { defer ctxDone() - err := routing.StartConfigMapRoutingTableUpdater( - ctx, - lggr, - configMapInformer, - servingCfg.CurrentNamespace, - routingTable, - nil, - ) + err := routingTable.Start(ctx) lggr.Error(err, "config map routing table updater failed") return err }) @@ -149,13 +126,8 @@ func main() { err := runAdminServer( ctx, lggr, - configMapsInterface, q, - routingTable, - deployCache, adminPort, - servingCfg, - timeoutCfg, ) lggr.Error(err, "admin server failed") return err @@ -194,43 +166,16 @@ func main() { func runAdminServer( ctx context.Context, lggr logr.Logger, - cmGetter k8s.ConfigMapGetter, q queue.Counter, - routingTable *routing.Table, - deployCache k8s.DeploymentCache, port int, - servingConfig *config.Serving, - timeoutConfig *config.Timeouts, ) error { lggr = lggr.WithName("runAdminServer") - adminServer := nethttp.NewServeMux() + adminServer := http.NewServeMux() queue.AddCountsRoute( lggr, adminServer, q, ) - routing.AddFetchRoute( - lggr, - adminServer, - routingTable, - ) - routing.AddPingRoute( - lggr, - adminServer, - cmGetter, - routingTable, - q, - ) - adminServer.HandleFunc( - "/deployments", - func(w nethttp.ResponseWriter, r *nethttp.Request) { - if err := json.NewEncoder(w).Encode(deployCache); err != nil { - lggr.Error(err, "encoding deployment cache") - } - }, - ) - kedahttp.AddConfigEndpoint(lggr, adminServer, servingConfig, timeoutConfig) - kedahttp.AddVersionEndpoint(lggr.WithName("interceptorAdmin"), adminServer) addr := fmt.Sprintf("0.0.0.0:%d", port) lggr.Info("admin server starting", "address", addr) @@ -239,30 +184,45 @@ func runAdminServer( func runProxyServer( ctx context.Context, - lggr logr.Logger, + logger logr.Logger, q queue.Counter, waitFunc forwardWaitFunc, - routingTable *routing.Table, + routingTable routing.Table, timeouts *config.Timeouts, port int, ) error { - lggr = lggr.WithName("runProxyServer") dialer := kedanet.NewNetDialer(timeouts.Connect, timeouts.KeepAlive) dialContextFunc := kedanet.DialContextWithRetry(dialer, timeouts.DefaultBackoff()) - proxyHdl := countMiddleware( - lggr, + + probeHandler := handler.NewProbe([]util.HealthChecker{ + routingTable, + }) + go probeHandler.Start(ctx) + + var upstreamHandler http.Handler + upstreamHandler = newForwardingHandler( + logger, + dialContextFunc, + waitFunc, + newForwardingConfigFromTimeouts(timeouts), + ) + upstreamHandler = middleware.NewCountingMiddleware( q, - newForwardingHandler( - lggr, - routingTable, - dialContextFunc, - waitFunc, - routing.ServiceURL, - newForwardingConfigFromTimeouts(timeouts), - ), + upstreamHandler, + ) + + var rootHandler http.Handler + rootHandler = middleware.NewRouting( + routingTable, + probeHandler, + upstreamHandler, + ) + rootHandler = middleware.NewLogging( + logger, + rootHandler, ) addr := fmt.Sprintf("0.0.0.0:%d", port) - lggr.Info("proxy server starting", "address", addr) - return kedahttp.ServeContext(ctx, addr, proxyHdl) + logger.Info("proxy server starting", "address", addr) + return kedahttp.ServeContext(ctx, addr, rootHandler) } diff --git a/interceptor/main_test.go b/interceptor/main_test.go index f86531c15..09910a0b1 100644 --- a/interceptor/main_test.go +++ b/interceptor/main_test.go @@ -2,9 +2,7 @@ package main import ( "context" - "encoding/json" "fmt" - "io" "net/http" "strconv" "testing" @@ -13,21 +11,16 @@ import ( "github.com/go-logr/logr" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/rand" "github.com/kedacore/http-add-on/interceptor/config" "github.com/kedacore/http-add-on/pkg/k8s" kedanet "github.com/kedacore/http-add-on/pkg/net" "github.com/kedacore/http-add-on/pkg/queue" - "github.com/kedacore/http-add-on/pkg/routing" - "github.com/kedacore/http-add-on/pkg/test" + routingtest "github.com/kedacore/http-add-on/pkg/routing/test" ) func TestRunProxyServerCountMiddleware(t *testing.T) { const ( - ns = "testns" port = 8080 host = "samplehost" ) @@ -49,20 +42,22 @@ func TestRunProxyServerCountMiddleware(t *testing.T) { r.NoError(err) g, ctx := errgroup.WithContext(ctx) q := queue.NewFakeCounter() - routingTable := routing.NewTable() + + httpso := targetFromURL( + originURL, + originPort, + "testdepl", + ) + namespacedName := k8s.NamespacedNameFromObject(httpso).String() + // set up a fake host that we can spoof // when we later send request to the proxy, // so that the proxy calculates a URL for that // host that points to the (above) fake origin - // server. - r.NoError(routingTable.AddTarget( - host, - targetFromURL( - originURL, - originPort, - "testdepl", - ), - )) + // server + routingTable := routingtest.NewTable() + routingTable.Memory[host] = httpso + timeouts := &config.Timeouts{} waiterCh := make(chan struct{}) waitFunc := func(_ context.Context, _, _ string) (int, error) { @@ -114,18 +109,22 @@ func TestRunProxyServerCountMiddleware(t *testing.T) { time.Sleep(100 * time.Millisecond) select { case hostAndCount := <-q.ResizedCh: - r.Equal(host, hostAndCount.Host) + r.Equal(namespacedName, hostAndCount.Host) r.Equal(+1, hostAndCount.Count) case <-time.After(500 * time.Millisecond): r.Fail("timeout waiting for +1 queue resize") } // tell the wait func to proceed - waiterCh <- struct{}{} + select { + case waiterCh <- struct{}{}: + case <-time.After(5 * time.Second): + r.Fail("timeout producing on waiterCh") + } select { case hostAndCount := <-q.ResizedCh: - r.Equal(host, hostAndCount.Host) + r.Equal(namespacedName, hostAndCount.Host) r.Equal(-1, hostAndCount.Count) case <-time.After(2 * time.Second): r.Fail("timeout waiting for -1 queue resize") @@ -136,136 +135,14 @@ func TestRunProxyServerCountMiddleware(t *testing.T) { r.NoError(err) counts := countsPtr.Counts r.Equal(1, len(counts)) - _, foundHost := counts[host] + _, foundHost := counts[namespacedName] r.True( foundHost, "couldn't find host %s in the queue", host, ) - r.Equal(0, counts[host]) - - done() - r.Error(g.Wait()) -} - -func TestRunAdminServerDeploymentsEndpoint(t *testing.T) { - const ( - ns = "testns" - ) - - ctx := context.Background() - ctx, done := context.WithCancel(ctx) - defer done() - lggr := logr.Discard() - r := require.New(t) - port := rand.Intn(100) + 8000 - const deplName = "testdeployment" - srvCfg := &config.Serving{} - timeoutCfg := &config.Timeouts{} - - deplCache := k8s.NewFakeDeploymentCache() - g, ctx := errgroup.WithContext(ctx) - - g.Go(func() error { - return runAdminServer( - ctx, - lggr, - k8s.FakeConfigMapGetter{}, - queue.NewFakeCounter(), - routing.NewTable(), - deplCache, - port, - srvCfg, - timeoutCfg, - ) - }) - time.Sleep(500 * time.Millisecond) - - deplCache.Set( - ns, - deplName, - appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: deplName, - }, - Spec: appsv1.DeploymentSpec{ - Replicas: k8s.Int32P(123), - }, - }, - ) - - res, err := http.Get(fmt.Sprintf("http://0.0.0.0:%d/deployments", port)) - r.NoError(err) - defer res.Body.Close() - r.Equal(200, res.StatusCode) - - actual := map[string]int32{} - r.NoError(json.NewDecoder(res.Body).Decode(&actual)) - - expected := map[string]int32{} - for name, depl := range deplCache.CurrentDeployments() { - expected[name] = *depl.Spec.Replicas - } - - r.Equal(expected, actual) + r.Equal(0, counts[namespacedName]) done() r.Error(g.Wait()) } - -func TestRunAdminServerConfig(t *testing.T) { - ctx := context.Background() - ctx, done := context.WithCancel(ctx) - defer done() - lggr := logr.Discard() - r := require.New(t) - const port = 8080 - srvCfg := &config.Serving{} - timeoutCfg := &config.Timeouts{} - - errgrp, ctx := errgroup.WithContext(ctx) - - errgrp.Go(func() error { - return runAdminServer( - ctx, - lggr, - k8s.FakeConfigMapGetter{}, - queue.NewFakeCounter(), - routing.NewTable(), - k8s.NewFakeDeploymentCache(), - port, - srvCfg, - timeoutCfg, - ) - }) - time.Sleep(500 * time.Millisecond) - - urlStr := func(path string) string { - return fmt.Sprintf("http://0.0.0.0:%d/%s", port, path) - } - res, err := http.Get(urlStr("config")) - r.NoError(err) - defer res.Body.Close() - r.Equal(200, res.StatusCode) - - bodyBytes, err := io.ReadAll(res.Body) - r.NoError(err) - - decodedIfaces := map[string][]interface{}{} - r.NoError(json.Unmarshal(bodyBytes, &decodedIfaces)) - r.Equal(1, len(decodedIfaces)) - _, hasKey := decodedIfaces["configs"] - r.True(hasKey, "config body doesn't have 'configs' key") - configs := decodedIfaces["configs"] - r.Equal(2, len(configs)) - - retSrvCfg := &config.Serving{} - r.NoError(test.JSONRoundTrip(configs[0], retSrvCfg)) - retTimeoutsCfg := &config.Timeouts{} - r.NoError(test.JSONRoundTrip(configs[1], retTimeoutsCfg)) - r.Equal(*srvCfg, *retSrvCfg) - r.Equal(*timeoutCfg, *retTimeoutsCfg) - - done() - r.Error(errgrp.Wait()) -} diff --git a/interceptor/middleware.go b/interceptor/middleware.go deleted file mode 100644 index 8a0ef24bf..000000000 --- a/interceptor/middleware.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" - - "github.com/go-logr/logr" - - "github.com/kedacore/http-add-on/pkg/queue" -) - -func getHost(r *http.Request) (string, error) { - // check the host header first, then the request host - // field (which may contain the actual URL if there is no - // host header) - if r.Header.Get("Host") != "" { - return r.Header.Get("Host"), nil - } - if r.Host != "" { - return r.Host, nil - } - return "", fmt.Errorf("host not found") -} - -// countMiddleware adds 1 to the given queue counter, executes next -// (by calling ServeHTTP on it), then decrements the queue counter -func countMiddleware( - lggr logr.Logger, - q queue.Counter, - next http.Handler, -) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - host, err := getHost(r) - if err != nil { - lggr.Error(err, "not forwarding request") - w.WriteHeader(400) - if _, err := w.Write([]byte("Host not found, not forwarding request")); err != nil { - lggr.Error(err, "could not write error message to client") - } - return - } - if err := q.Resize(host, +1); err != nil { - log.Printf("Error incrementing queue for %q (%s)", r.RequestURI, err) - } - defer func() { - if err := q.Resize(host, -1); err != nil { - log.Printf("Error decrementing queue for %q (%s)", r.RequestURI, err) - } - }() - next.ServeHTTP(w, r) - }) -} diff --git a/interceptor/middleware/counting.go b/interceptor/middleware/counting.go new file mode 100644 index 000000000..952924283 --- /dev/null +++ b/interceptor/middleware/counting.go @@ -0,0 +1,82 @@ +package middleware + +import ( + "context" + "net/http" + + "github.com/go-logr/logr" + + "github.com/kedacore/http-add-on/pkg/k8s" + "github.com/kedacore/http-add-on/pkg/queue" + "github.com/kedacore/http-add-on/pkg/util" +) + +type Counting struct { + queueCounter queue.Counter + upstreamHandler http.Handler +} + +func NewCountingMiddleware(queueCounter queue.Counter, upstreamHandler http.Handler) *Counting { + return &Counting{ + queueCounter: queueCounter, + upstreamHandler: upstreamHandler, + } +} + +var _ http.Handler = (*Counting)(nil) + +func (cm *Counting) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r = util.RequestWithLoggerWithName(r, "CountingMiddleware") + ctx := r.Context() + + defer cm.countAsync(ctx)() + + cm.upstreamHandler.ServeHTTP(w, r) +} + +func (cm *Counting) countAsync(ctx context.Context) func() { + signaler := util.NewSignaler() + + go cm.count(ctx, signaler) + + return func() { + go signaler.Signal() + } +} + +func (cm *Counting) count(ctx context.Context, signaler util.Signaler) { + logger := util.LoggerFromContext(ctx) + httpso := util.HTTPSOFromContext(ctx) + + key := k8s.NamespacedNameFromObject(httpso).String() + + if !cm.inc(logger, key) { + return + } + + if err := signaler.Wait(ctx); err != nil && err != context.Canceled { + logger.Error(err, "failed to wait signal") + } + + cm.dec(logger, key) +} + +func (cm *Counting) inc(logger logr.Logger, key string) bool { + if err := cm.queueCounter.Resize(key, +1); err != nil { + logger.Error(err, "error incrementing queue counter", "key", key) + + return false + } + + return true +} + +func (cm *Counting) dec(logger logr.Logger, key string) bool { + if err := cm.queueCounter.Resize(key, -1); err != nil { + logger.Error(err, "error decrementing queue counter", "key", key) + + return false + } + + return true +} diff --git a/interceptor/middleware_test.go b/interceptor/middleware/counting_test.go similarity index 70% rename from interceptor/middleware_test.go rename to interceptor/middleware/counting_test.go index 52224cae6..577a7370a 100644 --- a/interceptor/middleware_test.go +++ b/interceptor/middleware/counting_test.go @@ -1,4 +1,4 @@ -package main +package middleware import ( "context" @@ -6,23 +6,46 @@ import ( "math" "net/http" "net/http/httptest" + "net/url" "testing" "time" "github.com/go-logr/logr" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + "github.com/kedacore/http-add-on/pkg/k8s" "github.com/kedacore/http-add-on/pkg/queue" + "github.com/kedacore/http-add-on/pkg/util" ) func TestCountMiddleware(t *testing.T) { - ctx := context.Background() - const host = "testingkeda.com" r := require.New(t) + + uri, err := url.Parse("https://testingkeda.com") + r.NoError(err) + + httpso := &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "@", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + ScaleTargetRef: httpv1alpha1.ScaleTargetRef{ + Deployment: "testdepl", + Service: ":", + Port: 8080, + }, + TargetPendingRequests: pointer.Int32(123), + }, + } + namespacedName := k8s.NamespacedNameFromObject(httpso).String() + queueCounter := queue.NewFakeCounter() - middleware := countMiddleware( - logr.Discard(), + + middleware := NewCountingMiddleware( queueCounter, http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) { wr.WriteHeader(200) @@ -31,31 +54,23 @@ func TestCountMiddleware(t *testing.T) { }), ) - // no host in the request - req, err := http.NewRequest("GET", "/something", nil) - r.NoError(err) - agg, respRecorder := expectResizes( - ctx, - t, - 0, - middleware, - req, - queueCounter, - func(t *testing.T, hostAndCount queue.HostAndCount) {}, - ) - r.Equal(400, respRecorder.Code) - r.Equal("Host not found, not forwarding request", respRecorder.Body.String()) - r.Equal(0, agg) + ctx := context.Background() - // run middleware with the host in the request - req, err = http.NewRequest("GET", "/something", nil) - r.NoError(err) - req.Host = host // for a valid request, we expect the queue to be resized twice. // once to mark a pending HTTP request, then a second time to remove it. // by the end of both sends, resize1 + resize2 should be 0, // or in other words, the queue size should be back to zero - agg, respRecorder = expectResizes( + + // run middleware with the host in the request + req, err := http.NewRequest("GET", "/something", nil) + r.NoError(err) + reqCtx := req.Context() + reqCtx = util.ContextWithLogger(reqCtx, logr.Discard()) + reqCtx = util.ContextWithHTTPSO(reqCtx, httpso) + req = req.WithContext(reqCtx) + req.Host = uri.Host + + agg, respRecorder := expectResizes( ctx, t, 2, @@ -66,11 +81,11 @@ func TestCountMiddleware(t *testing.T) { t.Helper() r := require.New(t) r.Equal(float64(1), math.Abs(float64(hostAndCount.Count))) - r.Equal(host, hostAndCount.Host) + r.Equal(namespacedName, hostAndCount.Host) }, ) - r.Equal(200, respRecorder.Code) - r.Equal("OK", respRecorder.Body.String()) + r.Equal(http.StatusOK, respRecorder.Code) + r.Equal(http.StatusText(respRecorder.Code), respRecorder.Body.String()) r.Equal(0, agg) } @@ -95,7 +110,7 @@ func expectResizes( t.Helper() r := require.New(t) const timeout = 1 * time.Second - ctx, cancel := context.WithTimeout(ctx, 1*time.Second) + ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() grp, ctx := errgroup.WithContext(ctx) agg := 0 diff --git a/interceptor/middleware/logging.go b/interceptor/middleware/logging.go new file mode 100644 index 000000000..fd9730a1e --- /dev/null +++ b/interceptor/middleware/logging.go @@ -0,0 +1,74 @@ +package middleware + +import ( + "fmt" + "net/http" + + "github.com/go-logr/logr" + + "github.com/kedacore/http-add-on/pkg/util" +) + +const ( + CombinedLogFormat = `%s %s %s [%s] "%s %s %s" %d %d "%s" "%s"` + CombinedLogTimeFormat = "02/Jan/2006:15:04:05 -0700" + CombinedLogBlankValue = "-" +) + +type Logging struct { + logger logr.Logger + upstreamHandler http.Handler +} + +func NewLogging(logger logr.Logger, upstreamHandler http.Handler) *Logging { + return &Logging{ + logger: logger, + upstreamHandler: upstreamHandler, + } +} + +var _ http.Handler = (*Logging)(nil) + +func (lm *Logging) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r = util.RequestWithLogger(r, lm.logger.WithName("LoggingMiddleware")) + w = newLoggingResponseWriter(w) + + var sw util.Stopwatch + defer lm.logAsync(w, r, &sw) + + sw.Start() + defer sw.Stop() + + lm.upstreamHandler.ServeHTTP(w, r) +} + +func (lm *Logging) logAsync(w http.ResponseWriter, r *http.Request, sw *util.Stopwatch) { + go lm.log(w, r, sw) +} + +func (lm *Logging) log(w http.ResponseWriter, r *http.Request, sw *util.Stopwatch) { + ctx := r.Context() + logger := util.LoggerFromContext(ctx) + + lrw := w.(*loggingResponseWriter) + if lrw == nil { + lrw = newLoggingResponseWriter(w) + } + + timestamp := sw.StartTime().Format(CombinedLogTimeFormat) + log := fmt.Sprintf( + CombinedLogFormat, + r.RemoteAddr, + CombinedLogBlankValue, + CombinedLogBlankValue, + timestamp, + r.Method, + r.URL.Path, + r.Proto, + lrw.StatusCode(), + lrw.BytesWritten(), + r.Referer(), + r.UserAgent(), + ) + logger.Info(log) +} diff --git a/interceptor/middleware/loggingresponsewriter.go b/interceptor/middleware/loggingresponsewriter.go new file mode 100644 index 000000000..0ebb9c6c7 --- /dev/null +++ b/interceptor/middleware/loggingresponsewriter.go @@ -0,0 +1,45 @@ +package middleware + +import ( + "net/http" +) + +type loggingResponseWriter struct { + downstreamResponseWriter http.ResponseWriter + bytesWritten int + statusCode int +} + +func newLoggingResponseWriter(downstreamResponseWriter http.ResponseWriter) *loggingResponseWriter { + return &loggingResponseWriter{ + downstreamResponseWriter: downstreamResponseWriter, + } +} + +func (lrw *loggingResponseWriter) BytesWritten() int { + return lrw.bytesWritten +} + +func (lrw *loggingResponseWriter) StatusCode() int { + return lrw.statusCode +} + +var _ http.ResponseWriter = (*loggingResponseWriter)(nil) + +func (lrw *loggingResponseWriter) Header() http.Header { + return lrw.downstreamResponseWriter.Header() +} + +func (lrw *loggingResponseWriter) Write(bytes []byte) (int, error) { + n, err := lrw.downstreamResponseWriter.Write(bytes) + + lrw.bytesWritten += n + + return n, err +} + +func (lrw *loggingResponseWriter) WriteHeader(statusCode int) { + lrw.downstreamResponseWriter.WriteHeader(statusCode) + + lrw.statusCode = statusCode +} diff --git a/interceptor/middleware/loggingresponsewriter_test.go b/interceptor/middleware/loggingresponsewriter_test.go new file mode 100644 index 000000000..2a920a2f8 --- /dev/null +++ b/interceptor/middleware/loggingresponsewriter_test.go @@ -0,0 +1,122 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("loggingResponseWriter", func() { + Context("New", func() { + It("returns new object with expected field values set", func() { + var ( + w = httptest.NewRecorder() + ) + + lrw := newLoggingResponseWriter(w) + Expect(lrw).NotTo(BeNil()) + Expect(lrw.downstreamResponseWriter).To(Equal(w)) + Expect(lrw.bytesWritten).To(Equal(0)) + Expect(lrw.statusCode).To(Equal(0)) + }) + }) + + Context("BytesWritten", func() { + It("returns the expected value", func() { + const ( + bw = 128 + ) + + lrw := &loggingResponseWriter{ + bytesWritten: bw, + } + + ret := lrw.BytesWritten() + Expect(ret).To(Equal(bw)) + }) + }) + + Context("StatusCode", func() { + It("returns the expected value", func() { + const ( + sc = http.StatusTeapot + ) + + lrw := &loggingResponseWriter{ + statusCode: sc, + } + + ret := lrw.StatusCode() + Expect(ret).To(Equal(sc)) + }) + }) + + Context("Header", func() { + It("returns downstream method call", func() { + var ( + w = httptest.NewRecorder() + ) + + lrw := &loggingResponseWriter{ + downstreamResponseWriter: w, + } + + h := w.Header() + h.Set("Content-Type", "application/json") + + ret := lrw.Header() + Expect(ret).To(Equal(h)) + }) + }) + + Context("Write", func() { + It("invokes downstream method, increases bytesWritten accordingly, and returns expected values", func() { + const ( + body = "KEDA" + bodyLen = len(body) + initialBW = 60 + ) + + var ( + w = httptest.NewRecorder() + ) + + lrw := &loggingResponseWriter{ + bytesWritten: initialBW, + downstreamResponseWriter: w, + } + + n, err := lrw.Write([]byte(body)) + Expect(err).To(BeNil()) + Expect(n).To(Equal(bodyLen)) + + Expect(lrw.bytesWritten).To(Equal(initialBW + bodyLen)) + + Expect(w.Body.String()).To(Equal(body)) + }) + }) + + Context("WriteHeader", func() { + It("invokes downstream method and records the value", func() { + const ( + sc = http.StatusTeapot + ) + + var ( + w = httptest.NewRecorder() + ) + + lrw := &loggingResponseWriter{ + statusCode: http.StatusOK, + downstreamResponseWriter: w, + } + lrw.WriteHeader(sc) + + Expect(lrw.statusCode).To(Equal(sc)) + + Expect(w.Code).To(Equal(sc)) + }) + }) +}) diff --git a/interceptor/middleware/routing.go b/interceptor/middleware/routing.go new file mode 100644 index 000000000..e332d13b3 --- /dev/null +++ b/interceptor/middleware/routing.go @@ -0,0 +1,77 @@ +package middleware + +import ( + "fmt" + "net/http" + "net/url" + "regexp" + + "github.com/kedacore/http-add-on/interceptor/handler" + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + "github.com/kedacore/http-add-on/pkg/routing" + "github.com/kedacore/http-add-on/pkg/util" +) + +var ( + kpUserAgent = regexp.MustCompile(`(^|\s)kube-probe/`) +) + +type Routing struct { + routingTable routing.Table + probeHandler http.Handler + upstreamHandler http.Handler +} + +func NewRouting(routingTable routing.Table, probeHandler http.Handler, upstreamHandler http.Handler) *Routing { + return &Routing{ + routingTable: routingTable, + probeHandler: probeHandler, + upstreamHandler: upstreamHandler, + } +} + +var _ http.Handler = (*Routing)(nil) + +func (rm *Routing) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r = util.RequestWithLoggerWithName(r, "RoutingMiddleware") + + httpso := rm.routingTable.Route(r) + if httpso == nil { + if rm.isKubeProbe(r) { + rm.probeHandler.ServeHTTP(w, r) + return + } + + sh := handler.NewStatic(http.StatusNotFound, nil) + sh.ServeHTTP(w, r) + + return + } + r = r.WithContext(util.ContextWithHTTPSO(r.Context(), httpso)) + + stream, err := rm.streamFromHTTPSO(httpso) + if err != nil { + sh := handler.NewStatic(http.StatusInternalServerError, err) + sh.ServeHTTP(w, r) + + return + } + r = r.WithContext(util.ContextWithStream(r.Context(), stream)) + + rm.upstreamHandler.ServeHTTP(w, r) +} + +func (rm *Routing) streamFromHTTPSO(httpso *httpv1alpha1.HTTPScaledObject) (*url.URL, error) { + //goland:noinspection HttpUrlsUsage + return url.Parse(fmt.Sprintf( + "http://%s.%s:%d", + httpso.Spec.ScaleTargetRef.Service, + httpso.GetNamespace(), + httpso.Spec.ScaleTargetRef.Port, + )) +} + +func (rm *Routing) isKubeProbe(r *http.Request) bool { + ua := r.UserAgent() + return kpUserAgent.Match([]byte(ua)) +} diff --git a/interceptor/middleware/routing_test.go b/interceptor/middleware/routing_test.go new file mode 100644 index 000000000..8caa7ede0 --- /dev/null +++ b/interceptor/middleware/routing_test.go @@ -0,0 +1,201 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + routingtest "github.com/kedacore/http-add-on/pkg/routing/test" +) + +var _ = Describe("RoutingMiddleware", func() { + Context("New", func() { + It("returns new object with expected fields", func() { + var ( + routingTable = routingtest.NewTable() + probeHandler = http.NewServeMux() + upstreamHandler = http.NewServeMux() + ) + probeHandler.Handle("/probe", http.HandlerFunc(nil)) + upstreamHandler.Handle("/upstream", http.HandlerFunc(nil)) + + rm := NewRouting(routingTable, probeHandler, upstreamHandler) + Expect(rm).NotTo(BeNil()) + Expect(rm.routingTable).To(Equal(routingTable)) + Expect(rm.probeHandler).To(Equal(probeHandler)) + Expect(rm.upstreamHandler).To(Equal(upstreamHandler)) + }) + }) + + Context("ServeHTTP", func() { + const ( + host = "keda.sh" + path = "/README" + ) + + var ( + upstreamHandler *http.ServeMux + probeHandler *http.ServeMux + routingTable *routingtest.Table + routingMiddleware *Routing + w *httptest.ResponseRecorder + r *http.Request + + httpso = httpv1alpha1.HTTPScaledObject{ + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + host, + }, + }, + } + ) + + BeforeEach(func() { + upstreamHandler = http.NewServeMux() + probeHandler = http.NewServeMux() + routingTable = routingtest.NewTable() + routingMiddleware = NewRouting(routingTable, probeHandler, upstreamHandler) + + w = httptest.NewRecorder() + + r = httptest.NewRequest(http.MethodGet, path, nil) + r.Host = host + }) + + When("route is found", func() { + It("routes to the upstream handler", func() { + var ( + sc = http.StatusTeapot + st = http.StatusText(sc) + ) + + var uh bool + upstreamHandler.Handle(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTeapot) + + _, err := w.Write([]byte(st)) + Expect(err).NotTo(HaveOccurred()) + + uh = true + })) + + var ph bool + probeHandler.Handle(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ph = true + })) + + routingTable.Memory[host] = &httpso + + routingMiddleware.ServeHTTP(w, r) + + Expect(uh).To(BeTrue()) + Expect(ph).To(BeFalse()) + Expect(w.Code).To(Equal(sc)) + Expect(w.Body.String()).To(Equal(st)) + }) + }) + + When("route is not found", func() { + It("routes to the probe handler", func() { + const ( + uaKey = "User-Agent" + uaVal = "kube-probe/0" + ) + + var ( + sc = http.StatusTeapot + st = http.StatusText(sc) + ) + + var uh bool + upstreamHandler.Handle(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + uh = true + })) + + var ph bool + probeHandler.Handle(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTeapot) + + _, err := w.Write([]byte(st)) + Expect(err).NotTo(HaveOccurred()) + + ph = true + })) + + r.Header.Set(uaKey, uaVal) + + routingMiddleware.ServeHTTP(w, r) + + Expect(uh).To(BeFalse()) + Expect(ph).To(BeTrue()) + Expect(w.Code).To(Equal(sc)) + Expect(w.Body.String()).To(Equal(st)) + }) + + It("serves 404", func() { + var ( + sc = http.StatusNotFound + st = http.StatusText(sc) + ) + + var uh bool + upstreamHandler.Handle(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + uh = true + })) + + var ph bool + probeHandler.Handle(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ph = true + })) + + routingMiddleware.ServeHTTP(w, r) + + Expect(uh).To(BeFalse()) + Expect(ph).To(BeFalse()) + Expect(w.Code).To(Equal(sc)) + Expect(w.Body.String()).To(Equal(st)) + }) + }) + }) + + Context("isKubeProbe", func() { + const ( + uaKey = "User-Agent" + ) + + var ( + r *http.Request + ) + + BeforeEach(func() { + r = httptest.NewRequest(http.MethodGet, "/", nil) + }) + + It("returns true if the request is from kube-probe", func() { + const ( + uaVal = "Go-http-client/1.1 kube-probe/1.27.1 (linux/amd64) kubernetes/4c94112" + ) + + r.Header.Set(uaKey, uaVal) + + var rm Routing + b := rm.isKubeProbe(r) + Expect(b).To(BeTrue()) + }) + + It("returns false if the request is not from kube-probe", func() { + const ( + uaVal = "Go-http-client/1.1 kubectl/v1.27.1 (linux/amd64) kubernetes/4c94112" + ) + + r.Header.Set(uaKey, uaVal) + + var rm Routing + b := rm.isKubeProbe(r) + Expect(b).To(BeFalse()) + }) + }) +}) diff --git a/interceptor/middleware/suite_test.go b/interceptor/middleware/suite_test.go new file mode 100644 index 000000000..05ad42a9c --- /dev/null +++ b/interceptor/middleware/suite_test.go @@ -0,0 +1,14 @@ +package middleware + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMiddleware(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Middleware Suite") +} diff --git a/interceptor/proxy_handlers.go b/interceptor/proxy_handlers.go index 7f7215c61..0b1306367 100644 --- a/interceptor/proxy_handlers.go +++ b/interceptor/proxy_handlers.go @@ -9,8 +9,9 @@ import ( "github.com/go-logr/logr" "github.com/kedacore/http-add-on/interceptor/config" + "github.com/kedacore/http-add-on/interceptor/handler" kedanet "github.com/kedacore/http-add-on/pkg/net" - "github.com/kedacore/http-add-on/pkg/routing" + "github.com/kedacore/http-add-on/pkg/util" ) type forwardingConfig struct { @@ -43,10 +44,8 @@ func newForwardingConfigFromTimeouts(t *config.Timeouts) forwardingConfig { // create a URL with url.Parse("https://...") func newForwardingHandler( lggr logr.Logger, - routingTable *routing.Table, dialCtxFunc kedanet.DialContextFunc, waitFunc forwardWaitFunc, - targetSvcURL routing.ServiceURLFunc, fwdCfg forwardingConfig, ) http.Handler { roundTripper := &http.Transport{ @@ -60,52 +59,31 @@ func newForwardingHandler( ResponseHeaderTimeout: fwdCfg.respHeaderTimeout, } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - host, err := getHost(r) - if err != nil { - w.WriteHeader(400) - if _, err := w.Write([]byte("Host not found in request")); err != nil { - lggr.Error(err, "could not write error response to client") - } - return - } - routingTarget, err := routingTable.Lookup(host) - if err != nil { - w.WriteHeader(404) - if _, err := w.Write([]byte(fmt.Sprintf("Host %s not found", r.Host))); err != nil { - lggr.Error(err, "could not send error message to client") - } - return - } + ctx := r.Context() + httpso := util.HTTPSOFromContext(ctx) waitFuncCtx, done := context.WithTimeout(r.Context(), fwdCfg.waitTimeout) defer done() replicas, err := waitFunc( waitFuncCtx, - routingTarget.Namespace, - routingTarget.Deployment, + httpso.GetNamespace(), + httpso.Spec.ScaleTargetRef.Deployment, ) if err != nil { lggr.Error(err, "wait function failed, not forwarding request") - w.WriteHeader(502) + w.WriteHeader(http.StatusBadGateway) if _, err := w.Write([]byte(fmt.Sprintf("error on backend (%s)", err))); err != nil { lggr.Error(err, "could not write error response to client") } return } - targetSvcURL, err := targetSvcURL(*routingTarget) - if err != nil { - lggr.Error(err, "forwarding failed") - w.WriteHeader(500) - if _, err := w.Write([]byte("error getting backend service URL")); err != nil { - lggr.Error(err, "could not write error response to client") - } - return - } isColdStart := "false" if replicas == 0 { isColdStart = "true" } w.Header().Add("X-KEDA-HTTP-Cold-Start", isColdStart) - forwardRequest(lggr, w, r, roundTripper, targetSvcURL) + + uh := handler.NewUpstream(roundTripper) + uh.ServeHTTP(w, r) }) } diff --git a/interceptor/proxy_handlers_integration_test.go b/interceptor/proxy_handlers_integration_test.go index 4e1456480..3777a5665 100644 --- a/interceptor/proxy_handlers_integration_test.go +++ b/interceptor/proxy_handlers_integration_test.go @@ -21,18 +21,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" + "github.com/kedacore/http-add-on/interceptor/middleware" "github.com/kedacore/http-add-on/pkg/k8s" kedanet "github.com/kedacore/http-add-on/pkg/net" - "github.com/kedacore/http-add-on/pkg/routing" + routingtest "github.com/kedacore/http-add-on/pkg/routing/test" ) // happy path - deployment is scaled to 1 and host in routing table func TestIntegrationHappyPath(t *testing.T) { const ( deploymentReplicasTimeout = 200 * time.Millisecond - responseHeaderTimeout = 1 * time.Second deplName = "testdeployment" - namespace = "testns" ) r := require.New(t) h, err := newHarness( @@ -43,7 +42,17 @@ func TestIntegrationHappyPath(t *testing.T) { defer h.close() t.Logf("Harness: %s", h.String()) - h.deplCache.Set(namespace, deplName, appsv1.Deployment{ + originPort, err := strconv.Atoi(h.originURL.Port()) + r.NoError(err) + + target := targetFromURL( + h.originURL, + originPort, + deplName, + ) + h.routingTable.Memory[hostForTest(t)] = target + + h.deplCache.Set(target.GetNamespace(), deplName, appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: deplName}, Spec: appsv1.DeploymentSpec{ // note that the forwarding wait function doesn't care about @@ -57,14 +66,6 @@ func TestIntegrationHappyPath(t *testing.T) { }, }) - originPort, err := strconv.Atoi(h.originURL.Port()) - r.NoError(err) - r.NoError(h.routingTable.AddTarget(hostForTest(t), targetFromURL( - h.originURL, - originPort, - deplName, - ))) - // happy path res, err := doRequest( http.DefaultClient, @@ -83,20 +84,10 @@ func TestIntegrationHappyPath(t *testing.T) { // need to set the replicas to 1, but we're doing so anyway to // isolate the routing table behavior func TestIntegrationNoRoutingTableEntry(t *testing.T) { - const ( - ns = "testns" - ) - host := fmt.Sprintf("%s.integrationtest.interceptor.kedahttp.dev", t.Name()) r := require.New(t) h, err := newHarness(t, time.Second) r.NoError(err) defer h.close() - h.deplCache.Set(ns, host, appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: host}, - Spec: appsv1.DeploymentSpec{ - Replicas: i32Ptr(1), - }, - }) // not in the routing table res, err := doRequest( @@ -114,7 +105,6 @@ func TestIntegrationNoRoutingTableEntry(t *testing.T) { func TestIntegrationNoReplicas(t *testing.T) { const ( deployTimeout = 100 * time.Millisecond - ns = "testns" ) host := hostForTest(t) deployName := "testdeployment" @@ -124,14 +114,16 @@ func TestIntegrationNoReplicas(t *testing.T) { originPort, err := strconv.Atoi(h.originURL.Port()) r.NoError(err) - r.NoError(h.routingTable.AddTarget(hostForTest(t), targetFromURL( + + target := targetFromURL( h.originURL, originPort, deployName, - ))) + ) + h.routingTable.Memory[hostForTest(t)] = target // 0 replicas - h.deplCache.Set(ns, deployName, appsv1.Deployment{ + h.deplCache.Set(target.GetNamespace(), deployName, appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: deployName}, Spec: appsv1.DeploymentSpec{ Replicas: i32Ptr(0), @@ -159,7 +151,6 @@ func TestIntegrationWaitReplicas(t *testing.T) { const ( deployTimeout = 2 * time.Second responseTimeout = 1 * time.Second - ns = "testns" deployName = "testdeployment" ) ctx := context.Background() @@ -170,14 +161,13 @@ func TestIntegrationWaitReplicas(t *testing.T) { // add host to routing table originPort, err := strconv.Atoi(h.originURL.Port()) r.NoError(err) - r.NoError(h.routingTable.AddTarget( - hostForTest(t), - targetFromURL( - h.originURL, - originPort, - deployName, - ), - )) + + target := targetFromURL( + h.originURL, + originPort, + deployName, + ) + h.routingTable.Memory[hostForTest(t)] = target // set up a deployment with zero replicas and create // a watcher we can use later to fake-send a deployment @@ -188,8 +178,8 @@ func TestIntegrationWaitReplicas(t *testing.T) { Replicas: i32Ptr(0), }, } - h.deplCache.Set(ns, deployName, initialDeployment) - watcher := h.deplCache.SetWatcher(ns, deployName) + h.deplCache.Set(target.GetNamespace(), deployName, initialDeployment) + watcher := h.deplCache.SetWatcher(target.GetNamespace(), deployName) // make the request in one goroutine, and in the other, wait a bit // and then add replicas to the deployment cache @@ -265,7 +255,7 @@ type harness struct { originHdl http.Handler originSrv *httptest.Server originURL *url.URL - routingTable *routing.Table + routingTable *routingtest.Table dialCtxFunc kedanet.DialContextFunc deplCache *k8s.FakeDeploymentCache waitFunc forwardWaitFunc @@ -277,7 +267,7 @@ func newHarness( ) (*harness, error) { t.Helper() lggr := logr.Discard() - routingTable := routing.NewTable() + routingTable := routingtest.NewTable() dialContextFunc := kedanet.DialContextWithRetry( &net.Dialer{ Timeout: 2 * time.Second, @@ -306,19 +296,15 @@ func newHarness( return nil, err } - proxyHdl := newForwardingHandler( + proxyHdl := middleware.NewRouting(routingTable, nil, newForwardingHandler( lggr, - routingTable, dialContextFunc, waitFunc, - func(routing.Target) (*url.URL, error) { - return originSrvURL, nil - }, forwardingConfig{ waitTimeout: deployReplicasTimeout, respHeaderTimeout: time.Second, }, - ) + )) proxySrv, proxySrvURL, err := kedanet.StartTestServer(proxyHdl) if err != nil { diff --git a/interceptor/proxy_handlers_test.go b/interceptor/proxy_handlers_test.go index 25bf7d6ea..a303612dd 100644 --- a/interceptor/proxy_handlers_test.go +++ b/interceptor/proxy_handlers_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/http/httptest" "net/url" "strconv" "strings" @@ -12,9 +13,14 @@ import ( "github.com/go-logr/logr" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/utils/pointer" + "github.com/kedacore/http-add-on/interceptor/config" + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" kedanet "github.com/kedacore/http-add-on/pkg/net" - "github.com/kedacore/http-add-on/pkg/routing" + "github.com/kedacore/http-add-on/pkg/util" ) // the proxy should successfully forward a request to a running server @@ -32,15 +38,8 @@ func TestImmediatelySuccessfulProxy(t *testing.T) { srv, originURL, err := kedanet.StartTestServer(originHdl) r.NoError(err) defer srv.Close() - routingTable := routing.NewTable() originPort, err := strconv.Atoi(originURL.Port()) r.NoError(err) - target := targetFromURL( - originURL, - originPort, - "testdepl", - ) - r.NoError(routingTable.AddTarget(host, target)) timeouts := defaultTimeouts() dialCtxFunc := retryDialContextFunc(timeouts, timeouts.DefaultBackoff()) @@ -49,12 +48,8 @@ func TestImmediatelySuccessfulProxy(t *testing.T) { } hdl := newForwardingHandler( logr.Discard(), - routingTable, dialCtxFunc, waitFunc, - func(routing.Target) (*url.URL, error) { - return originURL, nil - }, forwardingConfig{ waitTimeout: timeouts.DeploymentReplicas, respHeaderTimeout: timeouts.ResponseHeader, @@ -62,8 +57,14 @@ func TestImmediatelySuccessfulProxy(t *testing.T) { ) const path = "/testfwd" res, req, err := reqAndRes(path) - req.Host = host r.NoError(err) + req = util.RequestWithHTTPSO(req, targetFromURL( + originURL, + originPort, + "testdepl", + )) + req = util.RequestWithStream(req, originURL) + req.Host = host hdl.ServeHTTP(res, req) @@ -88,32 +89,35 @@ func TestWaitFailedConnection(t *testing.T) { waitFunc := func(context.Context, string, string) (int, error) { return 1, nil } - routingTable := routing.NewTable() - r.NoError(routingTable.AddTarget(host, routing.NewTarget( - "testns", - "nosuchdepl", - 8081, - "nosuchdepl", - 1234, - ))) - hdl := newForwardingHandler( logr.Discard(), - routingTable, dialCtxFunc, waitFunc, - func(routing.Target) (*url.URL, error) { - return url.Parse("http://nosuchhost:8081") - }, forwardingConfig{ waitTimeout: timeouts.DeploymentReplicas, respHeaderTimeout: timeouts.ResponseHeader, }, ) + stream, err := url.Parse("http://0.0.0.0:0") + r.NoError(err) const path = "/testfwd" res, req, err := reqAndRes(path) - req.Host = host r.NoError(err) + req = util.RequestWithHTTPSO(req, &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "testns", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + ScaleTargetRef: httpv1alpha1.ScaleTargetRef{ + Deployment: "nosuchdepl", + Service: "nosuchdepl", + Port: 8081, + }, + TargetPendingRequests: pointer.Int32(1234), + }, + }) + req = util.RequestWithStream(req, stream) + req.Host = host hdl.ServeHTTP(res, req) @@ -127,38 +131,42 @@ func TestTimesOutOnWaitFunc(t *testing.T) { r := require.New(t) timeouts := defaultTimeouts() - timeouts.DeploymentReplicas = 1 * time.Millisecond - timeouts.ResponseHeader = 1 * time.Millisecond + timeouts.DeploymentReplicas = 25 * time.Millisecond + timeouts.ResponseHeader = 25 * time.Millisecond dialCtxFunc := retryDialContextFunc(timeouts, timeouts.DefaultBackoff()) waitFunc, waitFuncCalledCh, finishWaitFunc := notifyingFunc() defer finishWaitFunc() noSuchHost := fmt.Sprintf("%s.testing", t.Name()) - routingTable := routing.NewTable() - r.NoError(routingTable.AddTarget(noSuchHost, routing.NewTarget( - "testns", - "nosuchsvc", - 9091, - "nosuchdepl", - 1234, - ))) hdl := newForwardingHandler( logr.Discard(), - routingTable, dialCtxFunc, waitFunc, - func(routing.Target) (*url.URL, error) { - return url.Parse("http://nosuchhost:9091") - }, forwardingConfig{ waitTimeout: timeouts.DeploymentReplicas, respHeaderTimeout: timeouts.ResponseHeader, }, ) + stream, err := url.Parse("http://1.1.1.1") + r.NoError(err) const path = "/testfwd" res, req, err := reqAndRes(path) r.NoError(err) + req = util.RequestWithHTTPSO(req, &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "testns", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + ScaleTargetRef: httpv1alpha1.ScaleTargetRef{ + Deployment: "nosuchdepl", + Service: "nosuchsvc", + Port: 9091, + }, + TargetPendingRequests: pointer.Int32(1234), + }, + }) + req = util.RequestWithStream(req, stream) req.Host = noSuchHost start := time.Now() @@ -203,7 +211,6 @@ func TestWaitsForWaitFunc(t *testing.T) { const ( noSuchHost = "TestWaitsForWaitFunc.test" originRespCode = 201 - namespace = "testns" ) testSrv, testSrvURL, err := kedanet.StartTestServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -212,24 +219,12 @@ func TestWaitsForWaitFunc(t *testing.T) { ) r.NoError(err) defer testSrv.Close() - originHost, originPort, err := splitHostPort(testSrvURL.Host) + _, originPort, err := splitHostPort(testSrvURL.Host) r.NoError(err) - routingTable := routing.NewTable() - r.NoError(routingTable.AddTarget(noSuchHost, routing.NewTarget( - namespace, - originHost, - originPort, - "nosuchdepl", - 1234, - ))) hdl := newForwardingHandler( logr.Discard(), - routingTable, dialCtxFunc, waitFunc, - func(routing.Target) (*url.URL, error) { - return testSrvURL, nil - }, forwardingConfig{ waitTimeout: timeouts.DeploymentReplicas, respHeaderTimeout: timeouts.ResponseHeader, @@ -238,6 +233,12 @@ func TestWaitsForWaitFunc(t *testing.T) { const path = "/testfwd" res, req, err := reqAndRes(path) r.NoError(err) + req = util.RequestWithHTTPSO(req, targetFromURL( + testSrvURL, + originPort, + "nosuchdepl", + )) + req = util.RequestWithStream(req, testSrvURL) req.Host = noSuchHost // make the wait function finish after a short duration @@ -288,23 +289,10 @@ func TestWaitHeaderTimeout(t *testing.T) { waitFunc := func(context.Context, string, string) (int, error) { return 1, nil } - routingTable := routing.NewTable() - target := routing.NewTarget( - "testns", - "testsvc", - 9094, - "testdepl", - 1234, - ) - r.NoError(routingTable.AddTarget(originURL.Host, target)) hdl := newForwardingHandler( logr.Discard(), - routingTable, dialCtxFunc, waitFunc, - func(routing.Target) (*url.URL, error) { - return originURL, nil - }, forwardingConfig{ waitTimeout: timeouts.DeploymentReplicas, respHeaderTimeout: timeouts.ResponseHeader, @@ -313,6 +301,20 @@ func TestWaitHeaderTimeout(t *testing.T) { const path = "/testfwd" res, req, err := reqAndRes(path) r.NoError(err) + req = util.RequestWithHTTPSO(req, &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "testns", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + ScaleTargetRef: httpv1alpha1.ScaleTargetRef{ + Deployment: "nosuchdepl", + Service: "testsvc", + Port: 9094, + }, + TargetPendingRequests: pointer.Int32(1234), + }, + }) + req = util.RequestWithStream(req, originURL) req.Host = originURL.Host hdl.ServeHTTP(res, req) @@ -322,19 +324,6 @@ func TestWaitHeaderTimeout(t *testing.T) { close(originHdlCh) } -// ensureSignalAfter returns true if signalCh receives before timeout, false otherwise. -// it blocks for timeout at most -func ensureSignalBeforeTimeout(signalCh <-chan struct{}, timeout time.Duration) bool { - timer := time.NewTimer(timeout) - defer timer.Stop() - select { - case <-timer.C: - return false - case <-signalCh: - return true - } -} - func waitForSignal(sig <-chan struct{}, waitDur time.Duration) error { tmr := time.NewTimer(waitDur) defer tmr.Stop() @@ -376,13 +365,52 @@ func targetFromURL( u *url.URL, port int, deployment string, -) routing.Target { - svc := strings.Split(u.Host, ":")[0] - return routing.NewTarget( - "testns", - svc, - port, - deployment, - 123, +) *httpv1alpha1.HTTPScaledObject { + host := strings.Split(u.Host, ":")[0] + return &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "@" + host, + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + ScaleTargetRef: httpv1alpha1.ScaleTargetRef{ + Deployment: deployment, + Service: ":" + host, + Port: int32(port), + }, + TargetPendingRequests: pointer.Int32(123), + }, + } +} + +func defaultTimeouts() config.Timeouts { + return config.Timeouts{ + Connect: 100 * time.Millisecond, + KeepAlive: 100 * time.Millisecond, + ResponseHeader: 500 * time.Millisecond, + DeploymentReplicas: 1 * time.Second, + } +} + +// returns a kedanet.DialContextFunc by calling kedanet.DialContextWithRetry. if you pass nil for the +// timeoutConfig, it uses standard values. otherwise it uses the one you passed. +// +// the returned config.Timeouts is what was passed to the DialContextWithRetry function +func retryDialContextFunc( + timeouts config.Timeouts, + backoff wait.Backoff, +) kedanet.DialContextFunc { + dialer := kedanet.NewNetDialer( + timeouts.Connect, + timeouts.KeepAlive, ) + return kedanet.DialContextWithRetry(dialer, backoff) +} + +func reqAndRes(path string) (*httptest.ResponseRecorder, *http.Request, error) { + req, err := http.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + resRecorder := httptest.NewRecorder() + return resRecorder, req, nil } diff --git a/interceptor/request_forwarder.go b/interceptor/request_forwarder.go deleted file mode 100644 index 3a4afae08..000000000 --- a/interceptor/request_forwarder.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "net/http/httputil" - "net/url" - - "github.com/go-logr/logr" -) - -func forwardRequest( - lggr logr.Logger, - w http.ResponseWriter, - r *http.Request, - roundTripper http.RoundTripper, - fwdSvcURL *url.URL, -) { - proxy := httputil.NewSingleHostReverseProxy(fwdSvcURL) - proxy.Transport = roundTripper - proxy.Director = func(req *http.Request) { - req.URL = fwdSvcURL - req.Host = fwdSvcURL.Host - req.URL.Path = r.URL.Path - req.URL.RawQuery = r.URL.RawQuery - // delete the incoming X-Forwarded-For header so the proxy - // puts its own in. This is also important to prevent IP spoofing - req.Header.Del("X-Forwarded-For ") - } - proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { - w.WriteHeader(502) - // note: we can only use the '%w' directive inside of fmt.Errorf, - // not Sprintf or anything similar. this means we have to create the - // failure string in this slightly convoluted way. - errMsg := fmt.Errorf("error on backend (%w)", err).Error() - if _, err := w.Write([]byte(errMsg)); err != nil { - lggr.Error( - err, - "could not write error response to client", - ) - } - } - - proxy.ServeHTTP(w, r) -} diff --git a/interceptor/suite_test.go b/interceptor/suite_test.go new file mode 100644 index 000000000..86e4b9dad --- /dev/null +++ b/interceptor/suite_test.go @@ -0,0 +1,14 @@ +package main + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestInterceptor(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Interceptor Suite") +} diff --git a/operator/Dockerfile b/operator/Dockerfile index 23daa07e3..28474939f 100644 --- a/operator/Dockerfile +++ b/operator/Dockerfile @@ -1,36 +1,14 @@ -# Build the manager binary -FROM --platform=$BUILDPLATFORM ghcr.io/kedacore/build-tools:1.19.5 as builder -ARG VERSION=main -ARG GIT_COMMIT=HEAD - +FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/build-tools:1.20.4 as builder WORKDIR /workspace -# Copy the Go Modules manifests -COPY go.mod go.mod -COPY go.sum go.sum -# cache deps before building and copying source so that we don't need to re-download as much -# and so that source changes don't invalidate our downloaded layer +COPY go.* . RUN go mod download - -# Copy the go source -COPY operator operator -COPY pkg pkg -COPY proto proto -COPY Makefile Makefile - -# Build -# the ARCH has not a default value to allow the binary be built according to the host where the command -# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO -# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, -# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +COPY . . +ARG VERSION=main +ARG GIT_COMMIT=HEAD ARG TARGETOS ARG TARGETARCH -RUN VERSION=${VERSION} GIT_COMMIT=${GIT_COMMIT} TARGET_OS=${TARGETOS:-linux} ARCH=${TARGETARCH} make build-operator +RUN VERSION="${VERSION}" GIT_COMMIT="${GIT_COMMIT}" TARGET_OS="${TARGETOS}" ARCH="${TARGETARCH}" make build-operator -# Use distroless as minimal base image to package the manager binary -# Refer to https://github.com/GoogleContainerTools/distroless for more details FROM gcr.io/distroless/static:nonroot -WORKDIR / -COPY --from=builder /workspace/bin/operator . -USER 65532:65532 - -ENTRYPOINT ["/operator"] +COPY --from=builder /workspace/bin/operator /sbin/init +ENTRYPOINT ["/sbin/init"] diff --git a/operator/apis/http/v1alpha1/httpscaledobject_types.go b/operator/apis/http/v1alpha1/httpscaledobject_types.go index 850e71013..0ded0e90a 100644 --- a/operator/apis/http/v1alpha1/httpscaledobject_types.go +++ b/operator/apis/http/v1alpha1/httpscaledobject_types.go @@ -49,8 +49,7 @@ type HTTPScaledObjectSpec struct { // +optional Hosts []string `json:"hosts,omitempty"` // The name of the deployment to route HTTP requests to (and to autoscale). - // Either this or Image must be set - ScaleTargetRef *ScaleTargetRef `json:"scaleTargetRef"` + ScaleTargetRef ScaleTargetRef `json:"scaleTargetRef"` // (optional) Replica information // +optional Replicas *ReplicaStruct `json:"replicas,omitempty"` @@ -126,18 +125,18 @@ type HTTPScaledObjectStatus struct { Conditions []HTTPScaledObjectCondition `json:"conditions,omitempty" description:"List of auditable conditions of the operator"` } -//+genclient -//+k8s:openapi-gen=true -//+kubebuilder:object:root=true -//+kubebuilder:printcolumn:name="ScaleTargetDeploymentName",type="string",JSONPath=".spec.scaleTargetRef.deploymentName" -//+kubebuilder:printcolumn:name="ScaleTargetServiceName",type="string",JSONPath=".spec.scaleTargetRef" -//+kubebuilder:printcolumn:name="ScaleTargetPort",type="integer",JSONPath=".spec.scaleTargetRef" -//+kubebuilder:printcolumn:name="MinReplicas",type="integer",JSONPath=".spec.replicas.min" -//+kubebuilder:printcolumn:name="MaxReplicas",type="integer",JSONPath=".spec.replicas.max" -//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" -//+kubebuilder:printcolumn:name="Active",type="string",JSONPath=".status.conditions[?(@.type==\"HTTPScaledObjectIsReady\")].status" -//+kubebuilder:resource:shortName=httpso -//+kubebuilder:subresource:status +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:printcolumn:name="ScaleTargetDeploymentName",type="string",JSONPath=".spec.scaleTargetRef.deploymentName" +// +kubebuilder:printcolumn:name="ScaleTargetServiceName",type="string",JSONPath=".spec.scaleTargetRef" +// +kubebuilder:printcolumn:name="ScaleTargetPort",type="integer",JSONPath=".spec.scaleTargetRef" +// +kubebuilder:printcolumn:name="MinReplicas",type="integer",JSONPath=".spec.replicas.min" +// +kubebuilder:printcolumn:name="MaxReplicas",type="integer",JSONPath=".spec.replicas.max" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="Active",type="string",JSONPath=".status.conditions[?(@.type==\"HTTPScaledObjectIsReady\")].status" +// +kubebuilder:resource:shortName=httpso +// +kubebuilder:subresource:status // HTTPScaledObject is the Schema for the httpscaledobjects API type HTTPScaledObject struct { @@ -148,7 +147,7 @@ type HTTPScaledObject struct { Status HTTPScaledObjectStatus `json:"status,omitempty"` } -//+kubebuilder:object:root=true +// +kubebuilder:object:root=true // HTTPScaledObjectList contains a list of HTTPScaledObject type HTTPScaledObjectList struct { diff --git a/operator/apis/http/v1alpha1/zz_generated.deepcopy.go b/operator/apis/http/v1alpha1/zz_generated.deepcopy.go index 2d0ed2bf7..c135a4247 100644 --- a/operator/apis/http/v1alpha1/zz_generated.deepcopy.go +++ b/operator/apis/http/v1alpha1/zz_generated.deepcopy.go @@ -112,11 +112,7 @@ func (in *HTTPScaledObjectSpec) DeepCopyInto(out *HTTPScaledObjectSpec) { *out = make([]string, len(*in)) copy(*out, *in) } - if in.ScaleTargetRef != nil { - in, out := &in.ScaleTargetRef, &out.ScaleTargetRef - *out = new(ScaleTargetRef) - **out = **in - } + out.ScaleTargetRef = in.ScaleTargetRef if in.Replicas != nil { in, out := &in.Replicas, &out.Replicas *out = new(ReplicaStruct) diff --git a/operator/controllers/http/app.go b/operator/controllers/http/app.go index f10f42c04..ad66708e7 100644 --- a/operator/controllers/http/app.go +++ b/operator/controllers/http/app.go @@ -12,15 +12,12 @@ import ( "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" "github.com/kedacore/http-add-on/operator/controllers/http/config" - "github.com/kedacore/http-add-on/pkg/routing" ) func removeApplicationResources( ctx context.Context, logger logr.Logger, cl client.Client, - routingTable *routing.Table, - baseConfig config.Base, httpso *v1alpha1.HTTPScaledObject, ) error { defer SaveStatus(context.Background(), logger, cl, httpso) @@ -49,7 +46,7 @@ func removeApplicationResources( // Delete App ScaledObject scaledObject := &unstructured.Unstructured{} scaledObject.SetNamespace(httpso.Namespace) - scaledObject.SetName(config.AppScaledObjectName(httpso)) + scaledObject.SetName(httpso.Name) scaledObject.SetGroupVersionKind(schema.GroupVersionKind{ Group: "keda.sh", Kind: "ScaledObject", @@ -80,21 +77,13 @@ func removeApplicationResources( v1alpha1.AppScaledObjectTerminated, )) - return removeAndUpdateRoutingTable( - ctx, - logger, - cl, - routingTable, - httpso.Spec.Hosts, - baseConfig.CurrentNamespace, - ) + return nil } func createOrUpdateApplicationResources( ctx context.Context, logger logr.Logger, cl client.Client, - routingTable *routing.Table, baseConfig config.Base, externalScalerConfig config.ExternalScaler, httpso *v1alpha1.HTTPScaledObject, @@ -125,34 +114,11 @@ func createOrUpdateApplicationResources( // the app deployment and the interceptor deployment. // this needs to be submitted so that KEDA will scale both the app and // interceptor - if err := createOrUpdateScaledObject( + return createOrUpdateScaledObject( ctx, cl, logger, externalScalerConfig.HostName(baseConfig.CurrentNamespace), httpso, - ); err != nil { - return err - } - - targetPendingReqs := baseConfig.TargetPendingRequests - if tpr := httpso.Spec.TargetPendingRequests; tpr != nil { - targetPendingReqs = *tpr - } - - return addAndUpdateRoutingTable( - ctx, - logger, - cl, - routingTable, - httpso.Spec.Hosts, - routing.NewTarget( - httpso.GetNamespace(), - httpso.Spec.ScaleTargetRef.Service, - int(httpso.Spec.ScaleTargetRef.Port), - httpso.Spec.ScaleTargetRef.Deployment, - targetPendingReqs, - ), - baseConfig.CurrentNamespace, ) } diff --git a/operator/controllers/http/config/app_info.go b/operator/controllers/http/config/app_info.go deleted file mode 100644 index 3d4eff3ec..000000000 --- a/operator/controllers/http/config/app_info.go +++ /dev/null @@ -1,13 +0,0 @@ -package config - -import ( - "fmt" - - "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" -) - -// AppScaledObjectName returns the name of the ScaledObject -// that should be created alongside the given HTTPScaledObject. -func AppScaledObjectName(httpso *v1alpha1.HTTPScaledObject) string { - return fmt.Sprintf("%s-app", httpso.Spec.ScaleTargetRef.Deployment) -} diff --git a/operator/controllers/http/config/app_info_test.go b/operator/controllers/http/config/app_info_test.go deleted file mode 100644 index 7e7da558b..000000000 --- a/operator/controllers/http/config/app_info_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package config - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" -) - -func TestAppScaledObjectName(t *testing.T) { - r := require.New(t) - obj := &v1alpha1.HTTPScaledObject{ - Spec: v1alpha1.HTTPScaledObjectSpec{ - ScaleTargetRef: &v1alpha1.ScaleTargetRef{ - Deployment: "TestAppScaledObjectNameDeployment", - }, - }, - } - name := AppScaledObjectName(obj) - r.Equal( - fmt.Sprintf( - "%s-app", - obj.Spec.ScaleTargetRef.Deployment, - ), - name, - ) -} diff --git a/operator/controllers/http/httpscaledobject_controller.go b/operator/controllers/http/httpscaledobject_controller.go index 3e413734a..f1f6a98ac 100644 --- a/operator/controllers/http/httpscaledobject_controller.go +++ b/operator/controllers/http/httpscaledobject_controller.go @@ -21,7 +21,6 @@ import ( "errors" "time" - "github.com/go-logr/logr" k8serrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -33,7 +32,6 @@ import ( httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" "github.com/kedacore/http-add-on/operator/controllers/http/config" - "github.com/kedacore/http-add-on/pkg/routing" ) // HTTPScaledObjectReconciler reconciles a HTTPScaledObject object @@ -44,10 +42,8 @@ type HTTPScaledObjectReconciler struct { client.Client Scheme *runtime.Scheme - InterceptorConfig config.Interceptor ExternalScalerConfig config.ExternalScaler BaseConfig config.Base - RoutingTable *routing.Table } // +kubebuilder:rbac:groups=http.keda.sh,resources=httpscaledobjects,verbs=get;list;watch;create;update;patch;delete @@ -89,8 +85,6 @@ func (r *HTTPScaledObjectReconciler) Reconcile(ctx context.Context, req ctrl.Req ctx, logger, r.Client, - r.RoutingTable, - r.BaseConfig, httpso, ) if removeErr != nil { @@ -110,10 +104,10 @@ func (r *HTTPScaledObjectReconciler) Reconcile(ctx context.Context, req ctrl.Req return ctrl.Result{}, err } - // ensure only host or hosts is set and if host is set that - // it is converted to hosts - if err := sanitizeHosts(logger, httpso); err != nil { - return ctrl.Result{}, err + // TODO(pedrotorres): delete this on v0.6.0 + if httpso.Spec.Host != nil { + logger.Info(".spec.host is deprecated, performing automated migration to .spec.hosts") + return ctrl.Result{}, r.migrateHost(ctx, httpso) } // httpso is updated now @@ -130,7 +124,6 @@ func (r *HTTPScaledObjectReconciler) Reconcile(ctx context.Context, req ctrl.Req ctx, logger, r.Client, - r.RoutingTable, r.BaseConfig, r.ExternalScalerConfig, httpso, @@ -141,8 +134,6 @@ func (r *HTTPScaledObjectReconciler) Reconcile(ctx context.Context, req ctrl.Req ctx, logger, r.Client, - r.RoutingTable, - r.BaseConfig, httpso, ); removeErr != nil { logger.Error(removeErr, "Removing previously created resources") @@ -180,27 +171,16 @@ func (r *HTTPScaledObjectReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -// sanitize hosts by converting the host definition to hosts are erroring -// when both fields are set -func sanitizeHosts( - logger logr.Logger, - httpso *httpv1alpha1.HTTPScaledObject, -) error { - switch { - case httpso.Spec.Hosts != nil && httpso.Spec.Host != nil: - err := errors.New("mutually exclusive fields Error") - logger.Error(err, "Only one of 'hosts' or 'host' field can be defined") - return err - case httpso.Spec.Hosts == nil && httpso.Spec.Host == nil: - err := errors.New("no host specified Error") - logger.Error(err, "At least one of 'hosts' or 'host' field must be defined") - return err - case httpso.Spec.Hosts == nil: - httpso.Spec.Hosts = []string{*httpso.Spec.Host} - httpso.Spec.Host = nil - logger.Info("Using the 'host' field is deprecated. Please consider switching to the 'hosts' field") - return nil - default: - return nil +// TODO(pedrotorres): delete this on v0.6.0 +func (r *HTTPScaledObjectReconciler) migrateHost(ctx context.Context, httpso *httpv1alpha1.HTTPScaledObject) error { + if (httpso.Spec.Hosts != nil) == (httpso.Spec.Host != nil) { + return errors.New("exactly one of .spec.host and .spec.hosts must be set") + } + + httpso.Spec.Hosts = []string{ + *httpso.Spec.Host, } + httpso.Spec.Host = nil + + return r.Client.Update(ctx, httpso) } diff --git a/operator/controllers/http/httpscaledobject_test.go b/operator/controllers/http/httpscaledobject_test.go deleted file mode 100644 index 38650191b..000000000 --- a/operator/controllers/http/httpscaledobject_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package http - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestSanitizeHostsWithOnlyHosts(t *testing.T) { - r := require.New(t) - - testInfra := newCommonTestInfra("testns", "testapp") - spec := testInfra.httpso.Spec - - r.NoError(sanitizeHosts( - testInfra.logger, - &testInfra.httpso, - )) - - r.Equal(spec.Hosts, testInfra.httpso.Spec.Hosts) - r.Nil(testInfra.httpso.Spec.Host) -} - -func TestSanitizeHostsWithBothHostAndHosts(t *testing.T) { - r := require.New(t) - - testInfra := newBrokenTestInfra("testns", "testapp") - - err := sanitizeHosts( - testInfra.logger, - &testInfra.httpso, - ) - r.Error(err) -} - -func TestSanitizeHostsWithOnlyHost(t *testing.T) { - r := require.New(t) - - testInfra := newDeprecatedTestInfra("testns", "testapp") - spec := testInfra.httpso.Spec - - r.NoError(sanitizeHosts( - testInfra.logger, - &testInfra.httpso, - )) - - r.NotEqual(spec.Hosts, testInfra.httpso.Spec.Hosts) - r.NotEqual(spec.Host, testInfra.httpso.Spec.Host) - r.Nil(testInfra.httpso.Spec.Host) - r.Equal([]string{*spec.Host}, testInfra.httpso.Spec.Hosts) -} - -func TestSanitizeHostsWithNoHostOrHosts(t *testing.T) { - r := require.New(t) - - testInfra := newEmptyHostTestInfra("testns", "testapp") - - err := sanitizeHosts( - testInfra.logger, - &testInfra.httpso, - ) - r.Error(err) - r.Nil(testInfra.httpso.Spec.Host) - r.Nil(testInfra.httpso.Spec.Hosts) -} diff --git a/operator/controllers/http/routing_table.go b/operator/controllers/http/routing_table.go deleted file mode 100644 index 128c2a059..000000000 --- a/operator/controllers/http/routing_table.go +++ /dev/null @@ -1,84 +0,0 @@ -package http - -import ( - "context" - - "github.com/go-logr/logr" - pkgerrs "github.com/pkg/errors" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/kedacore/http-add-on/pkg/k8s" - "github.com/kedacore/http-add-on/pkg/routing" -) - -func removeAndUpdateRoutingTable( - ctx context.Context, - lggr logr.Logger, - cl client.Client, - table *routing.Table, - hosts []string, - namespace string, -) error { - lggr = lggr.WithName("removeAndUpdateRoutingTable") - for _, host := range hosts { - if err := table.RemoveTarget(host); err != nil { - lggr.Error( - err, - "could not remove host from routing table, progressing anyway", - "host", - host, - ) - } - } - - return updateRoutingMap(ctx, lggr, cl, namespace, table) -} - -func addAndUpdateRoutingTable( - ctx context.Context, - lggr logr.Logger, - cl client.Client, - table *routing.Table, - hosts []string, - target routing.Target, - namespace string, -) error { - lggr = lggr.WithName("addAndUpdateRoutingTable") - for _, host := range hosts { - if err := table.AddTarget(host, target); err != nil { - lggr.Error( - err, - "could not add host to routing table, progressing anyway", - "host", - host, - ) - } - } - return updateRoutingMap(ctx, lggr, cl, namespace, table) -} - -func updateRoutingMap( - ctx context.Context, - lggr logr.Logger, - cl client.Client, - namespace string, - table *routing.Table, -) error { - lggr = lggr.WithName("updateRoutingMap") - routingConfigMap, err := k8s.GetConfigMap(ctx, cl, namespace, routing.ConfigMapRoutingTableName) - if err != nil { - lggr.Error(err, "Error getting configmap", "configMapName", routing.ConfigMapRoutingTableName) - return pkgerrs.Wrap(err, "routing table ConfigMap fetch error") - } - newCM := routingConfigMap.DeepCopy() - if err := routing.SaveTableToConfigMap(table, newCM); err != nil { - lggr.Error(err, "couldn't save new routing table to ConfigMap", "configMap", routing.ConfigMapRoutingTableName) - return pkgerrs.Wrap(err, "ConfigMap save error") - } - if _, err := k8s.PatchConfigMap(ctx, lggr, cl, routingConfigMap, newCM); err != nil { - lggr.Error(err, "couldn't save new routing table ConfigMap to Kubernetes", "configMap", routing.ConfigMapRoutingTableName) - return pkgerrs.Wrap(err, "saving routing table ConfigMap to Kubernetes") - } - - return nil -} diff --git a/operator/controllers/http/routing_table_test.go b/operator/controllers/http/routing_table_test.go deleted file mode 100644 index 3f754ae38..000000000 --- a/operator/controllers/http/routing_table_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package http - -import ( - "context" - "testing" - - "github.com/go-logr/logr" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/kedacore/http-add-on/pkg/k8s" - "github.com/kedacore/http-add-on/pkg/routing" -) - -func getHosts() []string { - return []string{"myhost.com"} -} - -func TestRoutingTable(t *testing.T) { - table := routing.NewTable() - const ( - ns = "testns" - svcName = "testsvc" - deplName = "testdepl" - ) - hosts := getHosts() - r := require.New(t) - ctx := context.Background() - cl := k8s.NewFakeRuntimeClient() - cm := &corev1.ConfigMap{ - Data: map[string]string{}, - } - // save the empty routing table to the config map, - // so that it has valid structure - r.NoError(routing.SaveTableToConfigMap(table, cm)) - cl.GetFunc = func() client.Object { - return cm - } - target := routing.NewTarget( - ns, - svcName, - 8080, - deplName, - 1234, - ) - r.NoError(addAndUpdateRoutingTable( - ctx, - logr.Discard(), - cl, - table, - hosts, - target, - ns, - )) - // ensure that the ConfigMap was read and created. no updates - // should occur - r.Equal(1, len(cl.FakeRuntimeClientReader.GetCalls)) - r.Equal(1, len(cl.FakeRuntimeClientWriter.Patches)) - r.Equal(0, len(cl.FakeRuntimeClientWriter.Updates)) - r.Equal(0, len(cl.FakeRuntimeClientWriter.Creates)) - - for _, host := range hosts { - retTarget, err := table.Lookup(host) - r.NoError(err) - r.Equal(target, *retTarget) - } - - r.NoError(removeAndUpdateRoutingTable( - ctx, - logr.Discard(), - cl, - table, - hosts, - ns, - )) - - // ensure that the ConfigMap was read and updated. no additional - // creates should occur. - r.Equal(2, len(cl.FakeRuntimeClientReader.GetCalls)) - r.Equal(2, len(cl.FakeRuntimeClientWriter.Patches)) - r.Equal(0, len(cl.FakeRuntimeClientWriter.Updates)) - r.Equal(0, len(cl.FakeRuntimeClientWriter.Creates)) - - for _, host := range hosts { - _, err := table.Lookup(host) - r.Error(err) - } -} diff --git a/operator/controllers/http/scaled_object.go b/operator/controllers/http/scaled_object.go index fa4a881bb..7a536969a 100644 --- a/operator/controllers/http/scaled_object.go +++ b/operator/controllers/http/scaled_object.go @@ -10,7 +10,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" "github.com/kedacore/http-add-on/pkg/k8s" ) @@ -23,7 +23,7 @@ func createOrUpdateScaledObject( cl client.Client, logger logr.Logger, externalScalerHostName string, - httpso *v1alpha1.HTTPScaledObject, + httpso *httpv1alpha1.HTTPScaledObject, ) error { logger.Info("Creating scaled objects", "external scaler host name", externalScalerHostName) @@ -36,10 +36,14 @@ func createOrUpdateScaledObject( appScaledObject := k8s.NewScaledObject( httpso.GetNamespace(), - fmt.Sprintf("%s-app", httpso.GetName()), // HTTPScaledObject name is the same as the ScaledObject name + httpso.GetName(), // HTTPScaledObject name is the same as the ScaledObject name httpso.Spec.ScaleTargetRef.Deployment, externalScalerHostName, httpso.Spec.Hosts, + // TODO(pedrotorres): delete this when we support path prefix + nil, + // TODO(pedrotorres): uncomment this when we support path prefix + // httpso.Spec.PathPrefixes, minReplicaCount, maxReplicaCount, httpso.Spec.CooldownPeriod, @@ -72,9 +76,9 @@ func createOrUpdateScaledObject( httpso, *SetMessage( CreateCondition( - v1alpha1.Error, + httpv1alpha1.Error, v1.ConditionFalse, - v1alpha1.ErrorCreatingAppScaledObject, + httpv1alpha1.ErrorCreatingAppScaledObject, ), err.Error(), ), @@ -89,13 +93,50 @@ func createOrUpdateScaledObject( httpso, *SetMessage( CreateCondition( - v1alpha1.Created, + httpv1alpha1.Created, v1.ConditionTrue, - v1alpha1.AppScaledObjectCreated, + httpv1alpha1.AppScaledObjectCreated, ), "App ScaledObject created", ), ) + return purgeLegacySO(ctx, cl, logger, httpso) +} + +// TODO(pedrotorres): delete this on v0.6.0 +func purgeLegacySO( + ctx context.Context, + cl client.Client, + logger logr.Logger, + httpso *httpv1alpha1.HTTPScaledObject, +) error { + legacyName := fmt.Sprintf("%s-app", httpso.GetName()) + legacyKey := client.ObjectKey{ + Namespace: httpso.GetNamespace(), + Name: legacyName, + } + + var legacySO kedav1alpha1.ScaledObject + if err := cl.Get(ctx, legacyKey, &legacySO); err != nil { + if errors.IsNotFound(err) { + logger.Info("legacy ScaledObject not found") + return nil + } + + logger.Error(err, "failed getting legacy ScaledObject") + return err + } + + if err := cl.Delete(ctx, &legacySO); err != nil { + if errors.IsNotFound(err) { + logger.Info("legacy ScaledObject not found") + return nil + } + + logger.Error(err, "failed deleting legacy ScaledObject") + return err + } + return nil } diff --git a/operator/controllers/http/scaled_object_test.go b/operator/controllers/http/scaled_object_test.go index 07280bd20..3a3b1e288 100644 --- a/operator/controllers/http/scaled_object_test.go +++ b/operator/controllers/http/scaled_object_test.go @@ -11,7 +11,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" - "github.com/kedacore/http-add-on/operator/controllers/http/config" ) func TestCreateOrUpdateScaledObject(t *testing.T) { @@ -52,7 +51,7 @@ func TestCreateOrUpdateScaledObject(t *testing.T) { r.Equal(testInfra.ns, metadata.Namespace) r.Equal( - config.AppScaledObjectName(&testInfra.httpso), + testInfra.httpso.GetName(), metadata.Name, ) @@ -123,7 +122,7 @@ func getSO( var retSO kedav1alpha1.ScaledObject err := cl.Get(ctx, client.ObjectKey{ Namespace: httpso.GetNamespace(), - Name: config.AppScaledObjectName(&httpso), + Name: httpso.GetName(), }, &retSO) return &retSO, err } diff --git a/operator/controllers/http/suite_test.go b/operator/controllers/http/suite_test.go index 7a7f6ec11..b64d464a1 100644 --- a/operator/controllers/http/suite_test.go +++ b/operator/controllers/http/suite_test.go @@ -70,7 +70,7 @@ var _ = BeforeSuite(func() { err = kedav1alpha1.AddToScheme(clientgoscheme.Scheme) Expect(err).NotTo(HaveOccurred()) - //+kubebuilder:scaffold:scheme + // +kubebuilder:scaffold:scheme // k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) // Expect(err).NotTo(HaveOccurred()) @@ -108,7 +108,7 @@ func newCommonTestInfra(namespace, appName string) *commonTestInfra { Name: appName, }, Spec: httpv1alpha1.HTTPScaledObjectSpec{ - ScaleTargetRef: &httpv1alpha1.ScaleTargetRef{ + ScaleTargetRef: httpv1alpha1.ScaleTargetRef{ Deployment: appName, Service: appName, Port: 8081, @@ -126,112 +126,3 @@ func newCommonTestInfra(namespace, appName string) *commonTestInfra { httpso: httpso, } } - -func newBrokenTestInfra(namespace, appName string) *commonTestInfra { - localScheme := runtime.NewScheme() - utilruntime.Must(clientgoscheme.AddToScheme(localScheme)) - utilruntime.Must(httpv1alpha1.AddToScheme(localScheme)) - utilruntime.Must(kedav1alpha1.AddToScheme(localScheme)) - - ctx := context.Background() - cl := fake.NewClientBuilder().WithScheme(localScheme).Build() - logger := logr.Discard() - - host := "myhost1.com" - - httpso := httpv1alpha1.HTTPScaledObject{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: appName, - }, - Spec: httpv1alpha1.HTTPScaledObjectSpec{ - ScaleTargetRef: &httpv1alpha1.ScaleTargetRef{ - Deployment: appName, - Service: appName, - Port: 8081, - }, - Hosts: []string{"myhost1.com", "myhost2.com"}, - Host: &host, - }, - } - - return &commonTestInfra{ - ns: namespace, - appName: appName, - ctx: ctx, - cl: cl, - logger: logger, - httpso: httpso, - } -} - -func newDeprecatedTestInfra(namespace, appName string) *commonTestInfra { - localScheme := runtime.NewScheme() - utilruntime.Must(clientgoscheme.AddToScheme(localScheme)) - utilruntime.Must(httpv1alpha1.AddToScheme(localScheme)) - utilruntime.Must(kedav1alpha1.AddToScheme(localScheme)) - - ctx := context.Background() - cl := fake.NewClientBuilder().WithScheme(localScheme).Build() - logger := logr.Discard() - - host := "myhost1.com" - - httpso := httpv1alpha1.HTTPScaledObject{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: appName, - }, - Spec: httpv1alpha1.HTTPScaledObjectSpec{ - ScaleTargetRef: &httpv1alpha1.ScaleTargetRef{ - Deployment: appName, - Service: appName, - Port: 8081, - }, - Host: &host, - }, - } - - return &commonTestInfra{ - ns: namespace, - appName: appName, - ctx: ctx, - cl: cl, - logger: logger, - httpso: httpso, - } -} - -func newEmptyHostTestInfra(namespace, appName string) *commonTestInfra { - localScheme := runtime.NewScheme() - utilruntime.Must(clientgoscheme.AddToScheme(localScheme)) - utilruntime.Must(httpv1alpha1.AddToScheme(localScheme)) - utilruntime.Must(kedav1alpha1.AddToScheme(localScheme)) - - ctx := context.Background() - cl := fake.NewClientBuilder().WithScheme(localScheme).Build() - logger := logr.Discard() - - httpso := httpv1alpha1.HTTPScaledObject{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: appName, - }, - Spec: httpv1alpha1.HTTPScaledObjectSpec{ - ScaleTargetRef: &httpv1alpha1.ScaleTargetRef{ - Deployment: appName, - Service: appName, - Port: 8081, - }, - }, - } - - return &commonTestInfra{ - ns: namespace, - appName: appName, - ctx: ctx, - cl: cl, - logger: logger, - httpso: httpso, - } -} diff --git a/operator/generated/clientset/versioned/doc.go b/operator/generated/clientset/versioned/doc.go deleted file mode 100644 index d84fae187..000000000 --- a/operator/generated/clientset/versioned/doc.go +++ /dev/null @@ -1,20 +0,0 @@ -/* -Copyright 2023 The KEDA Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by client-gen. DO NOT EDIT. - -// This package has the automatically generated clientset. -package versioned diff --git a/operator/generated/clientset/versioned/mock/clientset.go b/operator/generated/clientset/versioned/mock/clientset.go new file mode 100644 index 000000000..6b7be9af2 --- /dev/null +++ b/operator/generated/clientset/versioned/mock/clientset.go @@ -0,0 +1,81 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/clientset/versioned/clientset.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1alpha1 "github.com/kedacore/http-add-on/operator/generated/clientset/versioned/typed/http/v1alpha1" + discovery "k8s.io/client-go/discovery" +) + +// MockInterface is a mock of Interface interface. +type MockInterface struct { + ctrl *gomock.Controller + recorder *MockInterfaceMockRecorder +} + +// MockInterfaceMockRecorder is the mock recorder for MockInterface. +type MockInterfaceMockRecorder struct { + mock *MockInterface +} + +// NewMockInterface creates a new mock instance. +func NewMockInterface(ctrl *gomock.Controller) *MockInterface { + mock := &MockInterface{ctrl: ctrl} + mock.recorder = &MockInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { + return m.recorder +} + +// Discovery mocks base method. +func (m *MockInterface) Discovery() discovery.DiscoveryInterface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Discovery") + ret0, _ := ret[0].(discovery.DiscoveryInterface) + return ret0 +} + +// Discovery indicates an expected call of Discovery. +func (mr *MockInterfaceMockRecorder) Discovery() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockInterface)(nil).Discovery)) +} + +// HttpV1alpha1 mocks base method. +func (m *MockInterface) HttpV1alpha1() v1alpha1.HttpV1alpha1Interface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HttpV1alpha1") + ret0, _ := ret[0].(v1alpha1.HttpV1alpha1Interface) + return ret0 +} + +// HttpV1alpha1 indicates an expected call of HttpV1alpha1. +func (mr *MockInterfaceMockRecorder) HttpV1alpha1() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HttpV1alpha1", reflect.TypeOf((*MockInterface)(nil).HttpV1alpha1)) +} diff --git a/operator/generated/clientset/versioned/scheme/mock/doc.go b/operator/generated/clientset/versioned/scheme/mock/doc.go new file mode 100644 index 000000000..4aa8810bb --- /dev/null +++ b/operator/generated/clientset/versioned/scheme/mock/doc.go @@ -0,0 +1,22 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/clientset/versioned/scheme/doc.go + +// Package mock is a generated GoMock package. +package mock diff --git a/operator/generated/clientset/versioned/scheme/mock/register.go b/operator/generated/clientset/versioned/scheme/mock/register.go new file mode 100644 index 000000000..1129b6cd2 --- /dev/null +++ b/operator/generated/clientset/versioned/scheme/mock/register.go @@ -0,0 +1,22 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/clientset/versioned/scheme/register.go + +// Package mock is a generated GoMock package. +package mock diff --git a/operator/generated/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscaledobject.go b/operator/generated/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscaledobject.go index f81f070c7..ac335d510 100644 --- a/operator/generated/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscaledobject.go +++ b/operator/generated/clientset/versioned/typed/http/v1alpha1/fake/fake_httpscaledobject.go @@ -24,7 +24,6 @@ import ( v1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" labels "k8s.io/apimachinery/pkg/labels" - schema "k8s.io/apimachinery/pkg/runtime/schema" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" testing "k8s.io/client-go/testing" @@ -36,9 +35,9 @@ type FakeHTTPScaledObjects struct { ns string } -var httpscaledobjectsResource = schema.GroupVersionResource{Group: "http", Version: "v1alpha1", Resource: "httpscaledobjects"} +var httpscaledobjectsResource = v1alpha1.SchemeGroupVersion.WithResource("httpscaledobjects") -var httpscaledobjectsKind = schema.GroupVersionKind{Group: "http", Version: "v1alpha1", Kind: "HTTPScaledObject"} +var httpscaledobjectsKind = v1alpha1.SchemeGroupVersion.WithKind("HTTPScaledObject") // Get takes name of the hTTPScaledObject, and returns the corresponding hTTPScaledObject object, and an error if there is any. func (c *FakeHTTPScaledObjects) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.HTTPScaledObject, err error) { diff --git a/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/doc.go b/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/doc.go new file mode 100644 index 000000000..557296cdf --- /dev/null +++ b/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/doc.go @@ -0,0 +1,22 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/clientset/versioned/typed/http/v1alpha1/doc.go + +// Package mock is a generated GoMock package. +package mock diff --git a/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/generated_expansion.go b/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/generated_expansion.go new file mode 100644 index 000000000..22c4cb49f --- /dev/null +++ b/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/generated_expansion.go @@ -0,0 +1,49 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/clientset/versioned/typed/http/v1alpha1/generated_expansion.go + +// Package mock is a generated GoMock package. +package mock + +import ( + gomock "github.com/golang/mock/gomock" +) + +// MockHTTPScaledObjectExpansion is a mock of HTTPScaledObjectExpansion interface. +type MockHTTPScaledObjectExpansion struct { + ctrl *gomock.Controller + recorder *MockHTTPScaledObjectExpansionMockRecorder +} + +// MockHTTPScaledObjectExpansionMockRecorder is the mock recorder for MockHTTPScaledObjectExpansion. +type MockHTTPScaledObjectExpansionMockRecorder struct { + mock *MockHTTPScaledObjectExpansion +} + +// NewMockHTTPScaledObjectExpansion creates a new mock instance. +func NewMockHTTPScaledObjectExpansion(ctrl *gomock.Controller) *MockHTTPScaledObjectExpansion { + mock := &MockHTTPScaledObjectExpansion{ctrl: ctrl} + mock.recorder = &MockHTTPScaledObjectExpansionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHTTPScaledObjectExpansion) EXPECT() *MockHTTPScaledObjectExpansionMockRecorder { + return m.recorder +} diff --git a/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/http_client.go b/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/http_client.go new file mode 100644 index 000000000..ffcf5ae00 --- /dev/null +++ b/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/http_client.go @@ -0,0 +1,81 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/clientset/versioned/typed/http/v1alpha1/http_client.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1alpha1 "github.com/kedacore/http-add-on/operator/generated/clientset/versioned/typed/http/v1alpha1" + rest "k8s.io/client-go/rest" +) + +// MockHttpV1alpha1Interface is a mock of HttpV1alpha1Interface interface. +type MockHttpV1alpha1Interface struct { + ctrl *gomock.Controller + recorder *MockHttpV1alpha1InterfaceMockRecorder +} + +// MockHttpV1alpha1InterfaceMockRecorder is the mock recorder for MockHttpV1alpha1Interface. +type MockHttpV1alpha1InterfaceMockRecorder struct { + mock *MockHttpV1alpha1Interface +} + +// NewMockHttpV1alpha1Interface creates a new mock instance. +func NewMockHttpV1alpha1Interface(ctrl *gomock.Controller) *MockHttpV1alpha1Interface { + mock := &MockHttpV1alpha1Interface{ctrl: ctrl} + mock.recorder = &MockHttpV1alpha1InterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHttpV1alpha1Interface) EXPECT() *MockHttpV1alpha1InterfaceMockRecorder { + return m.recorder +} + +// HTTPScaledObjects mocks base method. +func (m *MockHttpV1alpha1Interface) HTTPScaledObjects(namespace string) v1alpha1.HTTPScaledObjectInterface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HTTPScaledObjects", namespace) + ret0, _ := ret[0].(v1alpha1.HTTPScaledObjectInterface) + return ret0 +} + +// HTTPScaledObjects indicates an expected call of HTTPScaledObjects. +func (mr *MockHttpV1alpha1InterfaceMockRecorder) HTTPScaledObjects(namespace interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HTTPScaledObjects", reflect.TypeOf((*MockHttpV1alpha1Interface)(nil).HTTPScaledObjects), namespace) +} + +// RESTClient mocks base method. +func (m *MockHttpV1alpha1Interface) RESTClient() rest.Interface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RESTClient") + ret0, _ := ret[0].(rest.Interface) + return ret0 +} + +// RESTClient indicates an expected call of RESTClient. +func (mr *MockHttpV1alpha1InterfaceMockRecorder) RESTClient() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RESTClient", reflect.TypeOf((*MockHttpV1alpha1Interface)(nil).RESTClient)) +} diff --git a/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/httpscaledobject.go b/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/httpscaledobject.go new file mode 100644 index 000000000..bb802bd2c --- /dev/null +++ b/operator/generated/clientset/versioned/typed/http/v1alpha1/mock/httpscaledobject.go @@ -0,0 +1,232 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/clientset/versioned/typed/http/v1alpha1/httpscaledobject.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + v1alpha10 "github.com/kedacore/http-add-on/operator/generated/clientset/versioned/typed/http/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" +) + +// MockHTTPScaledObjectsGetter is a mock of HTTPScaledObjectsGetter interface. +type MockHTTPScaledObjectsGetter struct { + ctrl *gomock.Controller + recorder *MockHTTPScaledObjectsGetterMockRecorder +} + +// MockHTTPScaledObjectsGetterMockRecorder is the mock recorder for MockHTTPScaledObjectsGetter. +type MockHTTPScaledObjectsGetterMockRecorder struct { + mock *MockHTTPScaledObjectsGetter +} + +// NewMockHTTPScaledObjectsGetter creates a new mock instance. +func NewMockHTTPScaledObjectsGetter(ctrl *gomock.Controller) *MockHTTPScaledObjectsGetter { + mock := &MockHTTPScaledObjectsGetter{ctrl: ctrl} + mock.recorder = &MockHTTPScaledObjectsGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHTTPScaledObjectsGetter) EXPECT() *MockHTTPScaledObjectsGetterMockRecorder { + return m.recorder +} + +// HTTPScaledObjects mocks base method. +func (m *MockHTTPScaledObjectsGetter) HTTPScaledObjects(namespace string) v1alpha10.HTTPScaledObjectInterface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HTTPScaledObjects", namespace) + ret0, _ := ret[0].(v1alpha10.HTTPScaledObjectInterface) + return ret0 +} + +// HTTPScaledObjects indicates an expected call of HTTPScaledObjects. +func (mr *MockHTTPScaledObjectsGetterMockRecorder) HTTPScaledObjects(namespace interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HTTPScaledObjects", reflect.TypeOf((*MockHTTPScaledObjectsGetter)(nil).HTTPScaledObjects), namespace) +} + +// MockHTTPScaledObjectInterface is a mock of HTTPScaledObjectInterface interface. +type MockHTTPScaledObjectInterface struct { + ctrl *gomock.Controller + recorder *MockHTTPScaledObjectInterfaceMockRecorder +} + +// MockHTTPScaledObjectInterfaceMockRecorder is the mock recorder for MockHTTPScaledObjectInterface. +type MockHTTPScaledObjectInterfaceMockRecorder struct { + mock *MockHTTPScaledObjectInterface +} + +// NewMockHTTPScaledObjectInterface creates a new mock instance. +func NewMockHTTPScaledObjectInterface(ctrl *gomock.Controller) *MockHTTPScaledObjectInterface { + mock := &MockHTTPScaledObjectInterface{ctrl: ctrl} + mock.recorder = &MockHTTPScaledObjectInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHTTPScaledObjectInterface) EXPECT() *MockHTTPScaledObjectInterfaceMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockHTTPScaledObjectInterface) Create(ctx context.Context, hTTPScaledObject *v1alpha1.HTTPScaledObject, opts v1.CreateOptions) (*v1alpha1.HTTPScaledObject, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, hTTPScaledObject, opts) + ret0, _ := ret[0].(*v1alpha1.HTTPScaledObject) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockHTTPScaledObjectInterfaceMockRecorder) Create(ctx, hTTPScaledObject, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockHTTPScaledObjectInterface)(nil).Create), ctx, hTTPScaledObject, opts) +} + +// Delete mocks base method. +func (m *MockHTTPScaledObjectInterface) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, name, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockHTTPScaledObjectInterfaceMockRecorder) Delete(ctx, name, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockHTTPScaledObjectInterface)(nil).Delete), ctx, name, opts) +} + +// DeleteCollection mocks base method. +func (m *MockHTTPScaledObjectInterface) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteCollection", ctx, opts, listOpts) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteCollection indicates an expected call of DeleteCollection. +func (mr *MockHTTPScaledObjectInterfaceMockRecorder) DeleteCollection(ctx, opts, listOpts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCollection", reflect.TypeOf((*MockHTTPScaledObjectInterface)(nil).DeleteCollection), ctx, opts, listOpts) +} + +// Get mocks base method. +func (m *MockHTTPScaledObjectInterface) Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.HTTPScaledObject, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, name, opts) + ret0, _ := ret[0].(*v1alpha1.HTTPScaledObject) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockHTTPScaledObjectInterfaceMockRecorder) Get(ctx, name, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockHTTPScaledObjectInterface)(nil).Get), ctx, name, opts) +} + +// List mocks base method. +func (m *MockHTTPScaledObjectInterface) List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.HTTPScaledObjectList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, opts) + ret0, _ := ret[0].(*v1alpha1.HTTPScaledObjectList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockHTTPScaledObjectInterfaceMockRecorder) List(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockHTTPScaledObjectInterface)(nil).List), ctx, opts) +} + +// Patch mocks base method. +func (m *MockHTTPScaledObjectInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (*v1alpha1.HTTPScaledObject, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, name, pt, data, opts} + for _, a := range subresources { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Patch", varargs...) + ret0, _ := ret[0].(*v1alpha1.HTTPScaledObject) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Patch indicates an expected call of Patch. +func (mr *MockHTTPScaledObjectInterfaceMockRecorder) Patch(ctx, name, pt, data, opts interface{}, subresources ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, name, pt, data, opts}, subresources...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockHTTPScaledObjectInterface)(nil).Patch), varargs...) +} + +// Update mocks base method. +func (m *MockHTTPScaledObjectInterface) Update(ctx context.Context, hTTPScaledObject *v1alpha1.HTTPScaledObject, opts v1.UpdateOptions) (*v1alpha1.HTTPScaledObject, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, hTTPScaledObject, opts) + ret0, _ := ret[0].(*v1alpha1.HTTPScaledObject) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockHTTPScaledObjectInterfaceMockRecorder) Update(ctx, hTTPScaledObject, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockHTTPScaledObjectInterface)(nil).Update), ctx, hTTPScaledObject, opts) +} + +// UpdateStatus mocks base method. +func (m *MockHTTPScaledObjectInterface) UpdateStatus(ctx context.Context, hTTPScaledObject *v1alpha1.HTTPScaledObject, opts v1.UpdateOptions) (*v1alpha1.HTTPScaledObject, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateStatus", ctx, hTTPScaledObject, opts) + ret0, _ := ret[0].(*v1alpha1.HTTPScaledObject) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateStatus indicates an expected call of UpdateStatus. +func (mr *MockHTTPScaledObjectInterfaceMockRecorder) UpdateStatus(ctx, hTTPScaledObject, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockHTTPScaledObjectInterface)(nil).UpdateStatus), ctx, hTTPScaledObject, opts) +} + +// Watch mocks base method. +func (m *MockHTTPScaledObjectInterface) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Watch", ctx, opts) + ret0, _ := ret[0].(watch.Interface) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Watch indicates an expected call of Watch. +func (mr *MockHTTPScaledObjectInterfaceMockRecorder) Watch(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockHTTPScaledObjectInterface)(nil).Watch), ctx, opts) +} diff --git a/operator/generated/informers/externalversions/http/mock/interface.go b/operator/generated/informers/externalversions/http/mock/interface.go new file mode 100644 index 000000000..6993af756 --- /dev/null +++ b/operator/generated/informers/externalversions/http/mock/interface.go @@ -0,0 +1,66 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/informers/externalversions/http/interface.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1alpha1 "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1" +) + +// MockInterface is a mock of Interface interface. +type MockInterface struct { + ctrl *gomock.Controller + recorder *MockInterfaceMockRecorder +} + +// MockInterfaceMockRecorder is the mock recorder for MockInterface. +type MockInterfaceMockRecorder struct { + mock *MockInterface +} + +// NewMockInterface creates a new mock instance. +func NewMockInterface(ctrl *gomock.Controller) *MockInterface { + mock := &MockInterface{ctrl: ctrl} + mock.recorder = &MockInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { + return m.recorder +} + +// V1alpha1 mocks base method. +func (m *MockInterface) V1alpha1() v1alpha1.Interface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "V1alpha1") + ret0, _ := ret[0].(v1alpha1.Interface) + return ret0 +} + +// V1alpha1 indicates an expected call of V1alpha1. +func (mr *MockInterfaceMockRecorder) V1alpha1() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "V1alpha1", reflect.TypeOf((*MockInterface)(nil).V1alpha1)) +} diff --git a/operator/generated/informers/externalversions/http/v1alpha1/mock/httpscaledobject.go b/operator/generated/informers/externalversions/http/v1alpha1/mock/httpscaledobject.go new file mode 100644 index 000000000..985e8d58c --- /dev/null +++ b/operator/generated/informers/externalversions/http/v1alpha1/mock/httpscaledobject.go @@ -0,0 +1,81 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/informers/externalversions/http/v1alpha1/httpscaledobject.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1alpha1 "github.com/kedacore/http-add-on/operator/generated/listers/http/v1alpha1" + cache "k8s.io/client-go/tools/cache" +) + +// MockHTTPScaledObjectInformer is a mock of HTTPScaledObjectInformer interface. +type MockHTTPScaledObjectInformer struct { + ctrl *gomock.Controller + recorder *MockHTTPScaledObjectInformerMockRecorder +} + +// MockHTTPScaledObjectInformerMockRecorder is the mock recorder for MockHTTPScaledObjectInformer. +type MockHTTPScaledObjectInformerMockRecorder struct { + mock *MockHTTPScaledObjectInformer +} + +// NewMockHTTPScaledObjectInformer creates a new mock instance. +func NewMockHTTPScaledObjectInformer(ctrl *gomock.Controller) *MockHTTPScaledObjectInformer { + mock := &MockHTTPScaledObjectInformer{ctrl: ctrl} + mock.recorder = &MockHTTPScaledObjectInformerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHTTPScaledObjectInformer) EXPECT() *MockHTTPScaledObjectInformerMockRecorder { + return m.recorder +} + +// Informer mocks base method. +func (m *MockHTTPScaledObjectInformer) Informer() cache.SharedIndexInformer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Informer") + ret0, _ := ret[0].(cache.SharedIndexInformer) + return ret0 +} + +// Informer indicates an expected call of Informer. +func (mr *MockHTTPScaledObjectInformerMockRecorder) Informer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Informer", reflect.TypeOf((*MockHTTPScaledObjectInformer)(nil).Informer)) +} + +// Lister mocks base method. +func (m *MockHTTPScaledObjectInformer) Lister() v1alpha1.HTTPScaledObjectLister { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Lister") + ret0, _ := ret[0].(v1alpha1.HTTPScaledObjectLister) + return ret0 +} + +// Lister indicates an expected call of Lister. +func (mr *MockHTTPScaledObjectInformerMockRecorder) Lister() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lister", reflect.TypeOf((*MockHTTPScaledObjectInformer)(nil).Lister)) +} diff --git a/operator/generated/informers/externalversions/http/v1alpha1/mock/interface.go b/operator/generated/informers/externalversions/http/v1alpha1/mock/interface.go new file mode 100644 index 000000000..50aed1959 --- /dev/null +++ b/operator/generated/informers/externalversions/http/v1alpha1/mock/interface.go @@ -0,0 +1,66 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/informers/externalversions/http/v1alpha1/interface.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1alpha1 "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1" +) + +// MockInterface is a mock of Interface interface. +type MockInterface struct { + ctrl *gomock.Controller + recorder *MockInterfaceMockRecorder +} + +// MockInterfaceMockRecorder is the mock recorder for MockInterface. +type MockInterfaceMockRecorder struct { + mock *MockInterface +} + +// NewMockInterface creates a new mock instance. +func NewMockInterface(ctrl *gomock.Controller) *MockInterface { + mock := &MockInterface{ctrl: ctrl} + mock.recorder = &MockInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { + return m.recorder +} + +// HTTPScaledObjects mocks base method. +func (m *MockInterface) HTTPScaledObjects() v1alpha1.HTTPScaledObjectInformer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HTTPScaledObjects") + ret0, _ := ret[0].(v1alpha1.HTTPScaledObjectInformer) + return ret0 +} + +// HTTPScaledObjects indicates an expected call of HTTPScaledObjects. +func (mr *MockInterfaceMockRecorder) HTTPScaledObjects() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HTTPScaledObjects", reflect.TypeOf((*MockInterface)(nil).HTTPScaledObjects)) +} diff --git a/operator/generated/informers/externalversions/internalinterfaces/mock/factory_interfaces.go b/operator/generated/informers/externalversions/internalinterfaces/mock/factory_interfaces.go new file mode 100644 index 000000000..05417d055 --- /dev/null +++ b/operator/generated/informers/externalversions/internalinterfaces/mock/factory_interfaces.go @@ -0,0 +1,80 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/informers/externalversions/internalinterfaces/factory_interfaces.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + internalinterfaces "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/internalinterfaces" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" +) + +// MockSharedInformerFactory is a mock of SharedInformerFactory interface. +type MockSharedInformerFactory struct { + ctrl *gomock.Controller + recorder *MockSharedInformerFactoryMockRecorder +} + +// MockSharedInformerFactoryMockRecorder is the mock recorder for MockSharedInformerFactory. +type MockSharedInformerFactoryMockRecorder struct { + mock *MockSharedInformerFactory +} + +// NewMockSharedInformerFactory creates a new mock instance. +func NewMockSharedInformerFactory(ctrl *gomock.Controller) *MockSharedInformerFactory { + mock := &MockSharedInformerFactory{ctrl: ctrl} + mock.recorder = &MockSharedInformerFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSharedInformerFactory) EXPECT() *MockSharedInformerFactoryMockRecorder { + return m.recorder +} + +// InformerFor mocks base method. +func (m *MockSharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InformerFor", obj, newFunc) + ret0, _ := ret[0].(cache.SharedIndexInformer) + return ret0 +} + +// InformerFor indicates an expected call of InformerFor. +func (mr *MockSharedInformerFactoryMockRecorder) InformerFor(obj, newFunc interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InformerFor", reflect.TypeOf((*MockSharedInformerFactory)(nil).InformerFor), obj, newFunc) +} + +// Start mocks base method. +func (m *MockSharedInformerFactory) Start(stopCh <-chan struct{}) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Start", stopCh) +} + +// Start indicates an expected call of Start. +func (mr *MockSharedInformerFactoryMockRecorder) Start(stopCh interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockSharedInformerFactory)(nil).Start), stopCh) +} diff --git a/operator/generated/informers/externalversions/mock/factory.go b/operator/generated/informers/externalversions/mock/factory.go new file mode 100644 index 000000000..57db36fec --- /dev/null +++ b/operator/generated/informers/externalversions/mock/factory.go @@ -0,0 +1,138 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/informers/externalversions/factory.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + externalversions "github.com/kedacore/http-add-on/operator/generated/informers/externalversions" + http "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http" + internalinterfaces "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/internalinterfaces" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// MockSharedInformerFactory is a mock of SharedInformerFactory interface. +type MockSharedInformerFactory struct { + ctrl *gomock.Controller + recorder *MockSharedInformerFactoryMockRecorder +} + +// MockSharedInformerFactoryMockRecorder is the mock recorder for MockSharedInformerFactory. +type MockSharedInformerFactoryMockRecorder struct { + mock *MockSharedInformerFactory +} + +// NewMockSharedInformerFactory creates a new mock instance. +func NewMockSharedInformerFactory(ctrl *gomock.Controller) *MockSharedInformerFactory { + mock := &MockSharedInformerFactory{ctrl: ctrl} + mock.recorder = &MockSharedInformerFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSharedInformerFactory) EXPECT() *MockSharedInformerFactoryMockRecorder { + return m.recorder +} + +// ForResource mocks base method. +func (m *MockSharedInformerFactory) ForResource(resource schema.GroupVersionResource) (externalversions.GenericInformer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ForResource", resource) + ret0, _ := ret[0].(externalversions.GenericInformer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ForResource indicates an expected call of ForResource. +func (mr *MockSharedInformerFactoryMockRecorder) ForResource(resource interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForResource", reflect.TypeOf((*MockSharedInformerFactory)(nil).ForResource), resource) +} + +// Http mocks base method. +func (m *MockSharedInformerFactory) Http() http.Interface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Http") + ret0, _ := ret[0].(http.Interface) + return ret0 +} + +// Http indicates an expected call of Http. +func (mr *MockSharedInformerFactoryMockRecorder) Http() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Http", reflect.TypeOf((*MockSharedInformerFactory)(nil).Http)) +} + +// InformerFor mocks base method. +func (m *MockSharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InformerFor", obj, newFunc) + ret0, _ := ret[0].(cache.SharedIndexInformer) + return ret0 +} + +// InformerFor indicates an expected call of InformerFor. +func (mr *MockSharedInformerFactoryMockRecorder) InformerFor(obj, newFunc interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InformerFor", reflect.TypeOf((*MockSharedInformerFactory)(nil).InformerFor), obj, newFunc) +} + +// Shutdown mocks base method. +func (m *MockSharedInformerFactory) Shutdown() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Shutdown") +} + +// Shutdown indicates an expected call of Shutdown. +func (mr *MockSharedInformerFactoryMockRecorder) Shutdown() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockSharedInformerFactory)(nil).Shutdown)) +} + +// Start mocks base method. +func (m *MockSharedInformerFactory) Start(stopCh <-chan struct{}) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Start", stopCh) +} + +// Start indicates an expected call of Start. +func (mr *MockSharedInformerFactoryMockRecorder) Start(stopCh interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockSharedInformerFactory)(nil).Start), stopCh) +} + +// WaitForCacheSync mocks base method. +func (m *MockSharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WaitForCacheSync", stopCh) + ret0, _ := ret[0].(map[reflect.Type]bool) + return ret0 +} + +// WaitForCacheSync indicates an expected call of WaitForCacheSync. +func (mr *MockSharedInformerFactoryMockRecorder) WaitForCacheSync(stopCh interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForCacheSync", reflect.TypeOf((*MockSharedInformerFactory)(nil).WaitForCacheSync), stopCh) +} diff --git a/operator/generated/informers/externalversions/mock/generic.go b/operator/generated/informers/externalversions/mock/generic.go new file mode 100644 index 000000000..11ed83cc5 --- /dev/null +++ b/operator/generated/informers/externalversions/mock/generic.go @@ -0,0 +1,80 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/informers/externalversions/generic.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + cache "k8s.io/client-go/tools/cache" +) + +// MockGenericInformer is a mock of GenericInformer interface. +type MockGenericInformer struct { + ctrl *gomock.Controller + recorder *MockGenericInformerMockRecorder +} + +// MockGenericInformerMockRecorder is the mock recorder for MockGenericInformer. +type MockGenericInformerMockRecorder struct { + mock *MockGenericInformer +} + +// NewMockGenericInformer creates a new mock instance. +func NewMockGenericInformer(ctrl *gomock.Controller) *MockGenericInformer { + mock := &MockGenericInformer{ctrl: ctrl} + mock.recorder = &MockGenericInformerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGenericInformer) EXPECT() *MockGenericInformerMockRecorder { + return m.recorder +} + +// Informer mocks base method. +func (m *MockGenericInformer) Informer() cache.SharedIndexInformer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Informer") + ret0, _ := ret[0].(cache.SharedIndexInformer) + return ret0 +} + +// Informer indicates an expected call of Informer. +func (mr *MockGenericInformerMockRecorder) Informer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Informer", reflect.TypeOf((*MockGenericInformer)(nil).Informer)) +} + +// Lister mocks base method. +func (m *MockGenericInformer) Lister() cache.GenericLister { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Lister") + ret0, _ := ret[0].(cache.GenericLister) + return ret0 +} + +// Lister indicates an expected call of Lister. +func (mr *MockGenericInformerMockRecorder) Lister() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lister", reflect.TypeOf((*MockGenericInformer)(nil).Lister)) +} diff --git a/operator/generated/listers/http/v1alpha1/mock/expansion_generated.go b/operator/generated/listers/http/v1alpha1/mock/expansion_generated.go new file mode 100644 index 000000000..2b3357a03 --- /dev/null +++ b/operator/generated/listers/http/v1alpha1/mock/expansion_generated.go @@ -0,0 +1,72 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/listers/http/v1alpha1/expansion_generated.go + +// Package mock is a generated GoMock package. +package mock + +import ( + gomock "github.com/golang/mock/gomock" +) + +// MockHTTPScaledObjectListerExpansion is a mock of HTTPScaledObjectListerExpansion interface. +type MockHTTPScaledObjectListerExpansion struct { + ctrl *gomock.Controller + recorder *MockHTTPScaledObjectListerExpansionMockRecorder +} + +// MockHTTPScaledObjectListerExpansionMockRecorder is the mock recorder for MockHTTPScaledObjectListerExpansion. +type MockHTTPScaledObjectListerExpansionMockRecorder struct { + mock *MockHTTPScaledObjectListerExpansion +} + +// NewMockHTTPScaledObjectListerExpansion creates a new mock instance. +func NewMockHTTPScaledObjectListerExpansion(ctrl *gomock.Controller) *MockHTTPScaledObjectListerExpansion { + mock := &MockHTTPScaledObjectListerExpansion{ctrl: ctrl} + mock.recorder = &MockHTTPScaledObjectListerExpansionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHTTPScaledObjectListerExpansion) EXPECT() *MockHTTPScaledObjectListerExpansionMockRecorder { + return m.recorder +} + +// MockHTTPScaledObjectNamespaceListerExpansion is a mock of HTTPScaledObjectNamespaceListerExpansion interface. +type MockHTTPScaledObjectNamespaceListerExpansion struct { + ctrl *gomock.Controller + recorder *MockHTTPScaledObjectNamespaceListerExpansionMockRecorder +} + +// MockHTTPScaledObjectNamespaceListerExpansionMockRecorder is the mock recorder for MockHTTPScaledObjectNamespaceListerExpansion. +type MockHTTPScaledObjectNamespaceListerExpansionMockRecorder struct { + mock *MockHTTPScaledObjectNamespaceListerExpansion +} + +// NewMockHTTPScaledObjectNamespaceListerExpansion creates a new mock instance. +func NewMockHTTPScaledObjectNamespaceListerExpansion(ctrl *gomock.Controller) *MockHTTPScaledObjectNamespaceListerExpansion { + mock := &MockHTTPScaledObjectNamespaceListerExpansion{ctrl: ctrl} + mock.recorder = &MockHTTPScaledObjectNamespaceListerExpansionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHTTPScaledObjectNamespaceListerExpansion) EXPECT() *MockHTTPScaledObjectNamespaceListerExpansionMockRecorder { + return m.recorder +} diff --git a/operator/generated/listers/http/v1alpha1/mock/httpscaledobject.go b/operator/generated/listers/http/v1alpha1/mock/httpscaledobject.go new file mode 100644 index 000000000..4f4b7451b --- /dev/null +++ b/operator/generated/listers/http/v1alpha1/mock/httpscaledobject.go @@ -0,0 +1,136 @@ +// /* +// Copyright 2023 The KEDA Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// */ +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: operator/generated/listers/http/v1alpha1/httpscaledobject.go + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + v1alpha10 "github.com/kedacore/http-add-on/operator/generated/listers/http/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" +) + +// MockHTTPScaledObjectLister is a mock of HTTPScaledObjectLister interface. +type MockHTTPScaledObjectLister struct { + ctrl *gomock.Controller + recorder *MockHTTPScaledObjectListerMockRecorder +} + +// MockHTTPScaledObjectListerMockRecorder is the mock recorder for MockHTTPScaledObjectLister. +type MockHTTPScaledObjectListerMockRecorder struct { + mock *MockHTTPScaledObjectLister +} + +// NewMockHTTPScaledObjectLister creates a new mock instance. +func NewMockHTTPScaledObjectLister(ctrl *gomock.Controller) *MockHTTPScaledObjectLister { + mock := &MockHTTPScaledObjectLister{ctrl: ctrl} + mock.recorder = &MockHTTPScaledObjectListerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHTTPScaledObjectLister) EXPECT() *MockHTTPScaledObjectListerMockRecorder { + return m.recorder +} + +// HTTPScaledObjects mocks base method. +func (m *MockHTTPScaledObjectLister) HTTPScaledObjects(namespace string) v1alpha10.HTTPScaledObjectNamespaceLister { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HTTPScaledObjects", namespace) + ret0, _ := ret[0].(v1alpha10.HTTPScaledObjectNamespaceLister) + return ret0 +} + +// HTTPScaledObjects indicates an expected call of HTTPScaledObjects. +func (mr *MockHTTPScaledObjectListerMockRecorder) HTTPScaledObjects(namespace interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HTTPScaledObjects", reflect.TypeOf((*MockHTTPScaledObjectLister)(nil).HTTPScaledObjects), namespace) +} + +// List mocks base method. +func (m *MockHTTPScaledObjectLister) List(selector labels.Selector) ([]*v1alpha1.HTTPScaledObject, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", selector) + ret0, _ := ret[0].([]*v1alpha1.HTTPScaledObject) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockHTTPScaledObjectListerMockRecorder) List(selector interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockHTTPScaledObjectLister)(nil).List), selector) +} + +// MockHTTPScaledObjectNamespaceLister is a mock of HTTPScaledObjectNamespaceLister interface. +type MockHTTPScaledObjectNamespaceLister struct { + ctrl *gomock.Controller + recorder *MockHTTPScaledObjectNamespaceListerMockRecorder +} + +// MockHTTPScaledObjectNamespaceListerMockRecorder is the mock recorder for MockHTTPScaledObjectNamespaceLister. +type MockHTTPScaledObjectNamespaceListerMockRecorder struct { + mock *MockHTTPScaledObjectNamespaceLister +} + +// NewMockHTTPScaledObjectNamespaceLister creates a new mock instance. +func NewMockHTTPScaledObjectNamespaceLister(ctrl *gomock.Controller) *MockHTTPScaledObjectNamespaceLister { + mock := &MockHTTPScaledObjectNamespaceLister{ctrl: ctrl} + mock.recorder = &MockHTTPScaledObjectNamespaceListerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHTTPScaledObjectNamespaceLister) EXPECT() *MockHTTPScaledObjectNamespaceListerMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockHTTPScaledObjectNamespaceLister) Get(name string) (*v1alpha1.HTTPScaledObject, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", name) + ret0, _ := ret[0].(*v1alpha1.HTTPScaledObject) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockHTTPScaledObjectNamespaceListerMockRecorder) Get(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockHTTPScaledObjectNamespaceLister)(nil).Get), name) +} + +// List mocks base method. +func (m *MockHTTPScaledObjectNamespaceLister) List(selector labels.Selector) ([]*v1alpha1.HTTPScaledObject, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", selector) + ret0, _ := ret[0].([]*v1alpha1.HTTPScaledObject) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockHTTPScaledObjectNamespaceListerMockRecorder) List(selector interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockHTTPScaledObjectNamespaceLister)(nil).List), selector) +} diff --git a/operator/main.go b/operator/main.go index 25afb84b4..5cf7c44e2 100644 --- a/operator/main.go +++ b/operator/main.go @@ -17,17 +17,11 @@ limitations under the License. package main import ( - "context" "flag" - "fmt" - "net/http" "os" - "github.com/go-logr/logr" kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" - "golang.org/x/sync/errgroup" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -40,10 +34,6 @@ import ( httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" httpcontrollers "github.com/kedacore/http-add-on/operator/controllers/http" "github.com/kedacore/http-add-on/operator/controllers/http/config" - "github.com/kedacore/http-add-on/pkg/build" - kedahttp "github.com/kedacore/http-add-on/pkg/http" - "github.com/kedacore/http-add-on/pkg/k8s" - "github.com/kedacore/http-add-on/pkg/routing" // +kubebuilder:scaffold:imports ) @@ -89,11 +79,6 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - interceptorCfg, err := config.NewInterceptorFromEnv() - if err != nil { - setupLog.Error(err, "unable to get interceptor configuration") - os.Exit(1) - } externalScalerCfg, err := config.NewExternalScalerFromEnv() if err != nil { setupLog.Error(err, "unable to get external scaler configuration") @@ -131,15 +116,12 @@ func main() { os.Exit(1) } - routingTable := routing.NewTable() if err = (&httpcontrollers.HTTPScaledObjectReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - InterceptorConfig: *interceptorCfg, ExternalScalerConfig: *externalScalerCfg, BaseConfig: *baseConfig, - RoutingTable: routingTable, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "HTTPScaledObject") os.Exit(1) @@ -155,134 +137,9 @@ func main() { os.Exit(1) } - // TODO(pedrotorres): uncomment after implementing new routing table - // setupLog.Info("starting manager") - // if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { - // setupLog.Error(err, "problem running manager") - // os.Exit(1) - // } - - // TODO(pedrotorres): remove everything beyond this line after implementing - // new routing table - ctx := ctrl.SetupSignalHandler() - if err := ensureConfigMap( - ctx, - setupLog, - baseConfig.CurrentNamespace, - routing.ConfigMapRoutingTableName, - ); err != nil { - setupLog.Error( - err, - "unable to find routing table ConfigMap", - "namespace", - baseConfig.CurrentNamespace, - "name", - routing.ConfigMapRoutingTableName, - ) + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") os.Exit(1) } - - errGrp, ctx := errgroup.WithContext(ctx) - ctx, done := context.WithCancel(ctx) - - // start the control loop - errGrp.Go(func() error { - defer done() - setupLog.Info("starting manager") - return mgr.Start(ctx) - }) - - // start the admin server to serve routing table information - // to the interceptors - errGrp.Go(func() error { - defer done() - return runAdminServer( - ctx, - ctrl.Log, - routingTable, - adminPort, - baseConfig, - interceptorCfg, - externalScalerCfg, - ) - }) - build.PrintComponentInfo(setupLog, "Operator") - setupLog.Error(errGrp.Wait(), "running the operator") -} - -func runAdminServer( - ctx context.Context, - lggr logr.Logger, - routingTable *routing.Table, - port int, - baseCfg *config.Base, - interceptorCfg *config.Interceptor, - externalScalerCfg *config.ExternalScaler, - -) error { - mux := http.NewServeMux() - routing.AddFetchRoute(setupLog, mux, routingTable) - kedahttp.AddConfigEndpoint( - lggr.WithName("operatorAdmin"), - mux, - baseCfg, - interceptorCfg, - externalScalerCfg, - ) - kedahttp.AddVersionEndpoint(lggr.WithName("operatorAdmin"), mux) - addr := fmt.Sprintf(":%d", port) - lggr.Info( - "starting admin RPC server", - "port", - port, - ) - return kedahttp.ServeContext(ctx, addr, mux) -} - -// ensureConfigMap returns a non-nil error if the config -// map in the given namespace with the given name -// does not exist, or there was an error finding it. -// -// it returns a nil error if it could be fetched. -// this function works with its own Kubernetes client and -// is intended for use on operator startup, and should -// not be used with the controller library's client, -// since that is not usable until after the controller -// has started up. -func ensureConfigMap( - ctx context.Context, - lggr logr.Logger, - ns, - name string, -) error { - // we need to get our own Kubernetes clientset - // here, rather than using the client.Client from - // the manager because the client will not - // be instantiated by the time we call this. - // You need to start the manager before that client - // is usable. - clset, _, err := k8s.NewClientset() - if err != nil { - lggr.Error( - err, - "couldn't get new clientset", - ) - return err - } - if _, err := clset.CoreV1().ConfigMaps(ns).Get( - ctx, - name, - metav1.GetOptions{}, - ); err != nil { - lggr.Error( - err, - "couldn't find config map", - "namespace", - ns, - "name", - name, - ) - return err - } - return nil } diff --git a/operator/main_test.go b/operator/main_test.go deleted file mode 100644 index a42734c1a..000000000 --- a/operator/main_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/go-logr/logr" - "github.com/stretchr/testify/require" - "golang.org/x/sync/errgroup" - "k8s.io/apimachinery/pkg/util/rand" - - "github.com/kedacore/http-add-on/operator/controllers/http/config" - "github.com/kedacore/http-add-on/pkg/routing" - "github.com/kedacore/http-add-on/pkg/test" -) - -func TestRunAdminServerConfig(t *testing.T) { - ctx := context.Background() - ctx, done := context.WithCancel(ctx) - defer done() - lggr := logr.Discard() - r := require.New(t) - port := rand.Intn(100) + 8000 - baseCfg := &config.Base{} - interceptorCfg := &config.Interceptor{} - externalScalerCfg := &config.ExternalScaler{} - - errgrp, ctx := errgroup.WithContext(ctx) - - errgrp.Go(func() error { - return runAdminServer( - ctx, - lggr, - routing.NewTable(), - port, - baseCfg, - interceptorCfg, - externalScalerCfg, - ) - }) - time.Sleep(1 * time.Second) - - urlStr := func(path string) string { - return fmt.Sprintf("http://0.0.0.0:%d/%s", port, path) - } - res, err := http.Get(urlStr("config")) - r.NoError(err) - defer res.Body.Close() - r.Equal(200, res.StatusCode) - - bodyBytes, err := io.ReadAll(res.Body) - r.NoError(err) - - decodedIfaces := map[string][]interface{}{} - r.NoError(json.Unmarshal(bodyBytes, &decodedIfaces)) - r.Equal(1, len(decodedIfaces)) - _, hasKey := decodedIfaces["configs"] - r.True(hasKey, "config body doesn't have 'configs' key") - configs := decodedIfaces["configs"] - r.Equal(3, len(configs)) - - retBaseCfg := &config.Base{} - r.NoError(test.JSONRoundTrip(configs[0], retBaseCfg)) - retInterceptorCfg := &config.Interceptor{} - r.NoError(test.JSONRoundTrip(configs[1], retInterceptorCfg)) - retExternalScalerCfg := &config.ExternalScaler{} - r.NoError(test.JSONRoundTrip(configs[2], retExternalScalerCfg)) - r.Equal(*baseCfg, *retBaseCfg) - r.Equal(*interceptorCfg, *retInterceptorCfg) - r.Equal(*externalScalerCfg, *retExternalScalerCfg) - - done() - r.Error(errgrp.Wait()) -} diff --git a/pkg/http/config_endpoint.go b/pkg/http/config_endpoint.go deleted file mode 100644 index d574da2d4..000000000 --- a/pkg/http/config_endpoint.go +++ /dev/null @@ -1,36 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" - - "github.com/go-logr/logr" - - "github.com/kedacore/http-add-on/pkg/build" -) - -func AddConfigEndpoint(lggr logr.Logger, mux *http.ServeMux, configs ...interface{}) { - mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) { - if err := json.NewEncoder(w).Encode(map[string]interface{}{ - "configs": configs, - }); err != nil { - lggr.Error(err, "failed to encode configs") - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write([]byte(err.Error())) - lggr.Error(err, "failed sending encode version error") - } - }) -} - -func AddVersionEndpoint(lggr logr.Logger, mux *http.ServeMux) { - mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { - if err := json.NewEncoder(w).Encode(map[string]interface{}{ - "version": build.Version(), - }); err != nil { - lggr.Error(err, "failed to encode version") - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write([]byte(err.Error())) - lggr.Error(err, "failed sending encode version error") - } - }) -} diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go deleted file mode 100644 index 13c066ad0..000000000 --- a/pkg/k8s/client.go +++ /dev/null @@ -1,36 +0,0 @@ -package k8s - -import ( - "github.com/pkg/errors" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// NewClientset gets a new Kubernetes clientset, or calls log.Fatal -// if it couldn't -func NewClientset() (*kubernetes.Clientset, dynamic.Interface, error) { - // creates the in-cluster config - config, err := rest.InClusterConfig() - if err != nil { - return nil, nil, errors.Wrap(err, "Getting in-cluster config") - } - // creates the clientset - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, nil, errors.Wrap(err, "Creating k8s clientset") - } - dynamic, err := dynamic.NewForConfig(config) - - if err != nil { - return nil, nil, err - } - return clientset, dynamic, nil -} - -// ObjKey creates a new client.ObjectKey with the given -// name and namespace -func ObjKey(ns, name string) client.ObjectKey { - return client.ObjectKey{Namespace: ns, Name: name} -} diff --git a/pkg/k8s/client_fake.go b/pkg/k8s/client_fake.go deleted file mode 100644 index 6115ddd8b..000000000 --- a/pkg/k8s/client_fake.go +++ /dev/null @@ -1,177 +0,0 @@ -package k8s - -import ( - "context" - "encoding/json" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var _ client.Client = &FakeRuntimeClient{} -var _ client.Reader = &FakeRuntimeClientReader{} -var _ client.Writer = &FakeRuntimeClientWriter{} -var _ client.StatusClient = &FakeRuntimeStatusClient{} - -// FakeRuntimeClient is a fake implementation of -// (k8s.io/controller-runtime/pkg/client).Client -type FakeRuntimeClient struct { - *FakeRuntimeClientReader - *FakeRuntimeClientWriter - *FakeRuntimeStatusClient -} - -func NewFakeRuntimeClient() *FakeRuntimeClient { - return &FakeRuntimeClient{ - FakeRuntimeClientReader: &FakeRuntimeClientReader{}, - FakeRuntimeClientWriter: &FakeRuntimeClientWriter{}, - FakeRuntimeStatusClient: &FakeRuntimeStatusClient{}, - } -} - -// Scheme implements the controller-runtime Client interface. -// -// NOTE: this method is not implemented and always returns nil. -func (f *FakeRuntimeStatusClient) Scheme() *runtime.Scheme { - return nil -} - -// RESTMapper implements the controller-runtime Client interface. -// -// NOTE: this method is not implemented and always returns nil. -func (f *FakeRuntimeClientReader) RESTMapper() meta.RESTMapper { - return nil -} - -type GetCall struct { - Key client.ObjectKey - Obj client.Object -} - -// FakeRuntimeClientReader is a fake implementation of -// (k8s.io/controller-runtime/pkg/client).ClientReader -type FakeRuntimeClientReader struct { - GetCalls []GetCall - GetFunc func() client.Object - ListCalls []client.ObjectList - ListFunc func() client.ObjectList -} - -func (f *FakeRuntimeClientReader) Get( - ctx context.Context, - key client.ObjectKey, - obj client.Object, - opts ...client.GetOption, -) error { - f.GetCalls = append(f.GetCalls, GetCall{ - Key: key, - Obj: obj, - }) - // marshal the GetFunc return value, then unmarshal - // it back into the obj parameter. - b, err := json.Marshal(f.GetFunc()) - if err != nil { - return err - } - if err := json.Unmarshal(b, obj); err != nil { - return err - } - - return nil -} - -func (f *FakeRuntimeClientReader) List( - ctx context.Context, - list client.ObjectList, - opts ...client.ListOption, -) error { - f.ListCalls = append(f.ListCalls, list) - b, err := json.Marshal(f.ListFunc()) - if err != nil { - return err - } - if err := json.Unmarshal(b, list); err != nil { - return err - } - return nil -} - -// FakeRuntimeClientWriter is a fake implementation of -// (k8s.io/controller-runtime/pkg/client).ClientWriter -// -// It stores all method calls in the respective struct -// fields. Instances of FakeRuntimeClientWriter are not -// concurrency-safe -type FakeRuntimeClientWriter struct { - Creates []client.Object - Deletes []client.Object - Updates []client.Object - Patches []client.Object - DeleteAllOfs []client.Object -} - -func (f *FakeRuntimeClientWriter) Create( - ctx context.Context, - obj client.Object, - opts ...client.CreateOption, -) error { - f.Creates = append(f.Creates, obj) - return nil -} - -func (f *FakeRuntimeClientWriter) Delete( - ctx context.Context, - obj client.Object, - opts ...client.DeleteOption, -) error { - f.Deletes = append(f.Deletes, obj) - return nil -} - -func (f *FakeRuntimeClientWriter) Update( - ctx context.Context, - obj client.Object, - opts ...client.UpdateOption, -) error { - f.Updates = append(f.Updates, obj) - return nil -} - -func (f *FakeRuntimeClientWriter) Patch( - ctx context.Context, - obj client.Object, - patch client.Patch, - opts ...client.PatchOption, -) error { - f.Patches = append(f.Patches, obj) - return nil -} - -func (f *FakeRuntimeClientWriter) DeleteAllOf( - ctx context.Context, - obj client.Object, - opts ...client.DeleteAllOfOption, -) error { - f.DeleteAllOfs = append(f.DeleteAllOfs, obj) - return nil -} - -// FakeRuntimeStatusClient is a fake implementation of -// (k8s.io/controller-runtime/pkg/client).StatusClient -type FakeRuntimeStatusClient struct { -} - -// Status implements the controller-runtime StatusClient -// interface. -// -// NOTE: this function isn't implemented and always returns -// nil. -func (f *FakeRuntimeStatusClient) Status() client.StatusWriter { - return nil -} - -// SubResource implements client.Client -func (*FakeRuntimeClient) SubResource(subResource string) client.SubResourceClient { - panic("unimplemented") -} diff --git a/pkg/k8s/config_map.go b/pkg/k8s/config_map.go deleted file mode 100644 index 9335f4318..000000000 --- a/pkg/k8s/config_map.go +++ /dev/null @@ -1,79 +0,0 @@ -package k8s - -import ( - "context" - - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/watch" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// ConfigMapGetter is a pared down version of a ConfigMapInterface -// (found here: https://pkg.go.dev/k8s.io/client-go@v0.21.3/kubernetes/typed/core/v1#ConfigMapInterface). -// -// Pass this whenever possible to functions that only need to get individual ConfigMaps -// from Kubernetes, and nothing else. -type ConfigMapGetter interface { - Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.ConfigMap, error) -} - -// ConfigMapWatcher is a pared down version of a ConfigMapInterface -// (found here: https://pkg.go.dev/k8s.io/client-go@v0.21.3/kubernetes/typed/core/v1#ConfigMapInterface). -// -// Pass this whenever possible to functions that only need to watch for ConfigMaps -// from Kubernetes, and nothing else. -type ConfigMapWatcher interface { - Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) -} - -// ConfigMapGetterWatcher is a pared down version of a ConfigMapInterface -// (found here: https://pkg.go.dev/k8s.io/client-go@v0.21.3/kubernetes/typed/core/v1#ConfigMapInterface). -// -// Pass this whenever possible to functions that only need to watch for ConfigMaps -// from Kubernetes, and nothing else. -type ConfigMapGetterWatcher interface { - ConfigMapGetter - ConfigMapWatcher -} - -func PatchConfigMap( - ctx context.Context, - logger logr.Logger, - cl client.Writer, - originalConfigMap *corev1.ConfigMap, - patchConfigMap *corev1.ConfigMap, -) (*corev1.ConfigMap, error) { - logger = logger.WithName("pkg.k8s.PatchConfigMap") - if err := cl.Patch( - ctx, - patchConfigMap, - client.MergeFrom(originalConfigMap), - ); err != nil { - logger.Error( - err, - "failed to patch ConfigMap", - "originalConfigMap", - *originalConfigMap, - "patchConfigMap", - *patchConfigMap, - ) - return nil, err - } - return patchConfigMap, nil -} - -func GetConfigMap( - ctx context.Context, - cl client.Client, - namespace string, - name string, -) (*corev1.ConfigMap, error) { - configMap := &corev1.ConfigMap{} - err := cl.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, configMap) - if err != nil { - return nil, err - } - return configMap, nil -} diff --git a/pkg/k8s/config_map_cache_informer.go b/pkg/k8s/config_map_cache_informer.go deleted file mode 100644 index fd2bf6b71..000000000 --- a/pkg/k8s/config_map_cache_informer.go +++ /dev/null @@ -1,150 +0,0 @@ -package k8s - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/informers" - infcorev1 "k8s.io/client-go/informers/core/v1" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/cache" -) - -type InformerConfigMapUpdater struct { - lggr logr.Logger - cmInformer infcorev1.ConfigMapInformer - bcaster *watch.Broadcaster -} - -func (i *InformerConfigMapUpdater) MarshalJSON() ([]byte, error) { - lst := i.cmInformer.Lister() - cms, err := lst.List(labels.Everything()) - if err != nil { - return nil, err - } - return json.Marshal(&cms) -} - -func (i *InformerConfigMapUpdater) Start(ctx context.Context) error { - i.cmInformer.Informer().Run(ctx.Done()) - return errors.Wrap( - ctx.Err(), - "configMap informer was stopped", - ) -} - -func (i *InformerConfigMapUpdater) Get( - ns, - name string, -) (corev1.ConfigMap, error) { - cm, err := i.cmInformer.Lister().ConfigMaps(ns).Get(name) - if err != nil { - return corev1.ConfigMap{}, err - } - return *cm, nil -} - -func (i *InformerConfigMapUpdater) Watch( - ns, - name string, -) (watch.Interface, error) { - watched, err := i.bcaster.Watch() - if err != nil { - return nil, err - } - return watch.Filter(watched, func(e watch.Event) (watch.Event, bool) { - cm, ok := e.Object.(*corev1.ConfigMap) - if !ok { - i.lggr.Error( - fmt.Errorf("informer expected ConfigMap, ignoring this event"), - "event", - e, - ) - return e, false - } - if cm.Namespace == ns && cm.Name == name { - return e, true - } - return e, false - }), nil -} - -func (i *InformerConfigMapUpdater) addEvtHandler(obj interface{}) { - cm, ok := obj.(*corev1.ConfigMap) - if !ok { - i.lggr.Error( - fmt.Errorf("informer expected configMap, got %v", obj), - "not forwarding event", - ) - return - } - - if err := i.bcaster.Action(watch.Added, cm); err != nil { - i.lggr.Error(err, "informer expected configMap") - } -} - -func (i *InformerConfigMapUpdater) updateEvtHandler(oldObj, newObj interface{}) { - cm, ok := newObj.(*corev1.ConfigMap) - if !ok { - i.lggr.Error( - fmt.Errorf("informer expected configMap, got %v", newObj), - "not forwarding event", - ) - return - } - - if err := i.bcaster.Action(watch.Modified, cm); err != nil { - i.lggr.Error(err, "informer expected configMap") - } -} - -func (i *InformerConfigMapUpdater) deleteEvtHandler(obj interface{}) { - cm, ok := obj.(*corev1.ConfigMap) - if !ok { - i.lggr.Error( - fmt.Errorf("informer expected configMap, got %v", obj), - "not forwarding event", - ) - return - } - - if err := i.bcaster.Action(watch.Deleted, cm); err != nil { - i.lggr.Error(err, "informer expected configMap") - } -} - -func NewInformerConfigMapUpdater( - lggr logr.Logger, - cl kubernetes.Interface, - defaultResync time.Duration, - namespace string, -) *InformerConfigMapUpdater { - factory := informers.NewSharedInformerFactoryWithOptions( - cl, - defaultResync, - informers.WithNamespace(namespace), - ) - cmInformer := factory.Core().V1().ConfigMaps() - ret := &InformerConfigMapUpdater{ - lggr: lggr, - bcaster: watch.NewBroadcaster(0, watch.WaitIfChannelFull), - cmInformer: cmInformer, - } - _, err := ret.cmInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: ret.addEvtHandler, - UpdateFunc: ret.updateEvtHandler, - DeleteFunc: ret.deleteEvtHandler, - }) - if err != nil { - lggr.Error(err, "error creating config informer") - } - return ret -} diff --git a/pkg/k8s/config_map_fake.go b/pkg/k8s/config_map_fake.go deleted file mode 100644 index 1e3c77671..000000000 --- a/pkg/k8s/config_map_fake.go +++ /dev/null @@ -1,20 +0,0 @@ -package k8s - -import ( - "context" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type FakeConfigMapGetter struct { - ConfigMap *corev1.ConfigMap - Err error -} - -func (f FakeConfigMapGetter) Get(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.ConfigMap, error) { - if f.Err != nil { - return nil, f.Err - } - return f.ConfigMap, nil -} diff --git a/pkg/k8s/deployment_cache_informer.go b/pkg/k8s/deployment_cache_informer.go index 38ee17b84..80ca6b5cc 100644 --- a/pkg/k8s/deployment_cache_informer.go +++ b/pkg/k8s/deployment_cache_informer.go @@ -82,7 +82,7 @@ func (i *InformerBackedDeploymentCache) addEvtHandler(obj interface{}) { } } -func (i *InformerBackedDeploymentCache) updateEvtHandler(oldObj, newObj interface{}) { +func (i *InformerBackedDeploymentCache) updateEvtHandler(_, newObj interface{}) { depl, ok := newObj.(*appsv1.Deployment) if !ok { i.lggr.Error( diff --git a/pkg/k8s/namespacedname.go b/pkg/k8s/namespacedname.go new file mode 100644 index 000000000..42d887aa9 --- /dev/null +++ b/pkg/k8s/namespacedname.go @@ -0,0 +1,31 @@ +package k8s + +import ( + "github.com/kedacore/keda/v2/pkg/scalers/externalscaler" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kedacore/http-add-on/pkg/util" +) + +func NamespacedNameFromObject(obj client.Object) *types.NamespacedName { + if util.IsNil(obj) { + return nil + } + + return &types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + } +} + +func NamespacedNameFromScaledObjectRef(sor *externalscaler.ScaledObjectRef) *types.NamespacedName { + if sor == nil { + return nil + } + + return &types.NamespacedName{ + Namespace: sor.GetNamespace(), + Name: sor.GetName(), + } +} diff --git a/pkg/k8s/scaledobject.go b/pkg/k8s/scaledobject.go index e185a892f..97e3d7f30 100644 --- a/pkg/k8s/scaledobject.go +++ b/pkg/k8s/scaledobject.go @@ -15,6 +15,7 @@ const ( mkScalerAddress = "scalerAddress" mkHosts = "hosts" + mkPathPrefixes = "pathPrefixes" ) // NewScaledObject creates a new ScaledObject in memory @@ -24,6 +25,7 @@ func NewScaledObject( deploymentName string, scalerAddress string, hosts []string, + pathPrefixes []string, minReplicas *int32, maxReplicas *int32, cooldownPeriod *int32, @@ -57,6 +59,7 @@ func NewScaledObject( Metadata: map[string]string{ mkScalerAddress: scalerAddress, mkHosts: strings.Join(hosts, ","), + mkPathPrefixes: strings.Join(pathPrefixes, ","), }, }, }, diff --git a/pkg/queue/queue_rpc.go b/pkg/queue/queue_rpc.go index 542ebbdc3..4ff3ed9b6 100644 --- a/pkg/queue/queue_rpc.go +++ b/pkg/queue/queue_rpc.go @@ -1,7 +1,6 @@ package queue import ( - "context" "encoding/json" "fmt" "net/http" @@ -61,8 +60,6 @@ func newSizeHandler( // from the given hostAndPort. Note that the hostAndPort should // not end with a "/" and shouldn't include a path. func GetCounts( - ctx context.Context, - lggr logr.Logger, httpCl *http.Client, interceptorURL url.URL, ) (*Counts, error) { diff --git a/pkg/queue/queue_rpc_test.go b/pkg/queue/queue_rpc_test.go index 48887e6e4..e865ba612 100644 --- a/pkg/queue/queue_rpc_test.go +++ b/pkg/queue/queue_rpc_test.go @@ -1,7 +1,6 @@ package queue import ( - "context" "encoding/json" "errors" "testing" @@ -54,7 +53,6 @@ func TestQueueSizeHandlerFail(t *testing.T) { } func TestQueueSizeHandlerIntegration(t *testing.T) { - ctx := context.Background() lggr := logr.Discard() r := require.New(t) reader := &FakeCountReader{ @@ -67,7 +65,7 @@ func TestQueueSizeHandlerIntegration(t *testing.T) { r.NoError(err) defer srv.Close() httpCl := srv.Client() - counts, err := GetCounts(ctx, lggr, httpCl, *url) + counts, err := GetCounts(httpCl, *url) r.NoError(err) r.Equal(1, len(counts.Counts)) for _, val := range counts.Counts { diff --git a/pkg/routing/config_map.go b/pkg/routing/config_map.go deleted file mode 100644 index bb2660843..000000000 --- a/pkg/routing/config_map.go +++ /dev/null @@ -1,117 +0,0 @@ -package routing - -import ( - "context" - "fmt" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/kedacore/http-add-on/pkg/k8s" - "github.com/kedacore/http-add-on/pkg/queue" -) - -const ( - // the name of the ConfigMap that stores the routing table - ConfigMapRoutingTableName = "keda-http-add-on-routing-table" - // the key in the ConfigMap data that stores the JSON routing table - configMapRoutingTableKey = "routing-table" -) - -// SaveTableToConfigMap saves the contents of table to the Data field in -// configMap -func SaveTableToConfigMap(table *Table, configMap *corev1.ConfigMap) error { - tableAsJSON, err := table.MarshalJSON() - if err != nil { - return err - } - configMap.Data[configMapRoutingTableKey] = string(tableAsJSON) - return nil -} - -// FetchTableFromConfigMap fetches the Data field from configMap, converts it -// to a routing table, and returns it -func FetchTableFromConfigMap(configMap *corev1.ConfigMap) (*Table, error) { - data, found := configMap.Data[configMapRoutingTableKey] - if !found { - return nil, fmt.Errorf( - "no '%s' key found in the %s ConfigMap", - configMapRoutingTableKey, - ConfigMapRoutingTableName, - ) - } - ret := NewTable() - if err := ret.UnmarshalJSON([]byte(data)); err != nil { - retErr := errors.Wrap( - err, - fmt.Sprintf( - "error decoding '%s' key in %s ConfigMap", - configMapRoutingTableKey, - ConfigMapRoutingTableName, - ), - ) - return nil, retErr - } - return ret, nil -} - -// GetTable fetches the contents of the appropriate ConfigMap that stores -// the routing table, then tries to decode it into a temporary routing table -// data structure. -// -// If that succeeds, it calls table.Replace(newTable), then ensures that -// every host in the routing table exists in the given queue, and no hosts -// exist in the queue that don't exist in the routing table. It uses q.Ensure() -// and q.Remove() to do those things, respectively. -func GetTable( - ctx context.Context, - lggr logr.Logger, - getter k8s.ConfigMapGetter, - table *Table, - q queue.Counter, -) error { - lggr = lggr.WithName("pkg.routing.GetTable") - - cm, err := getter.Get( - ctx, - ConfigMapRoutingTableName, - metav1.GetOptions{}, - ) - if err != nil { - lggr.Error( - err, - "failed to fetch routing table config map", - "configMapName", - ConfigMapRoutingTableName, - ) - return errors.Wrap( - err, - fmt.Sprintf( - "failed to fetch ConfigMap %s", - ConfigMapRoutingTableName, - ), - ) - } - newTable, err := FetchTableFromConfigMap(cm) - if err != nil { - lggr.Error( - err, - "failed decoding routing table ConfigMap", - "configMapName", - ConfigMapRoutingTableName, - ) - return errors.Wrap( - err, - fmt.Sprintf( - "failed decoding ConfigMap %s into a routing table", - ConfigMapRoutingTableName, - ), - ) - } - - table.Replace(newTable) - - return nil -} diff --git a/pkg/routing/config_map_updater.go b/pkg/routing/config_map_updater.go deleted file mode 100644 index 50a5ac65b..000000000 --- a/pkg/routing/config_map_updater.go +++ /dev/null @@ -1,88 +0,0 @@ -package routing - -import ( - "context" - - "github.com/go-logr/logr" - "github.com/pkg/errors" - "golang.org/x/sync/errgroup" - corev1 "k8s.io/api/core/v1" - - "github.com/kedacore/http-add-on/pkg/k8s" -) - -// StartConfigMapRoutingTableUpdater starts a loop that does the following: -// -// - Fetches a full version of the ConfigMap called ConfigMapRoutingTableName in -// the given namespace ns, and calls table.Replace(newTable) after it does so -// - Uses watcher to watch for all ADDED or CREATED events on the ConfigMap -// called ConfigMapRoutingTableName. On either of those events, decodes -// that ConfigMap into a routing table and stores the new table into table -// using table.Replace(newTable) -// - Execute the callback function, if one exists -// - Returns an appropriate non-nil error if ctx.Done() receives -func StartConfigMapRoutingTableUpdater( - ctx context.Context, - lggr logr.Logger, - cmInformer *k8s.InformerConfigMapUpdater, - ns string, - table *Table, - cbFunc func() error, -) error { - lggr = lggr.WithName("pkg.routing.StartConfigMapRoutingTableUpdater") - - watcher, err := cmInformer.Watch(ns, ConfigMapRoutingTableName) - if err != nil { - return err - } - defer watcher.Stop() - - ctx, done := context.WithCancel(ctx) - defer done() - grp, ctx := errgroup.WithContext(ctx) - - grp.Go(func() error { - defer done() - return cmInformer.Start(ctx) - }) - - grp.Go(func() error { - defer done() - for { - select { - case event := <-watcher.ResultChan(): - cm, ok := event.Object.(*corev1.ConfigMap) - // Theoretically this will not happen - if !ok { - lggr.Info( - "The event object observed is not a configmap", - ) - continue - } - newTable, err := FetchTableFromConfigMap(cm) - if err != nil { - return err - } - table.Replace(newTable) - // Execute the callback function, if one exists - if cbFunc != nil { - if err := cbFunc(); err != nil { - lggr.Error( - err, - "failed to exec the callback function", - ) - continue - } - } - case <-ctx.Done(): - return errors.Wrap(ctx.Err(), "context is done") - } - } - }) - - if err := grp.Wait(); err != nil { - lggr.Error(err, "config map routing updater is failed") - return err - } - return nil -} diff --git a/pkg/routing/config_map_updater_test.go b/pkg/routing/config_map_updater_test.go deleted file mode 100644 index 28cf0f63e..000000000 --- a/pkg/routing/config_map_updater_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package routing - -import ( - "context" - "errors" - "strings" - "testing" - "time" - - "github.com/go-logr/logr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/sync/errgroup" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" - clgotesting "k8s.io/client-go/testing" - - "github.com/kedacore/http-add-on/pkg/k8s" - "github.com/kedacore/http-add-on/pkg/queue" -) - -// fake adapters for the k8s.GetterWatcher interface. -// -// Note that there is another way to fake the k8s getter and -// watcher types. -// -// we could use the "fake" package in k8s.io/client-go -// (https://pkg.go.dev/k8s.io/client-go@v0.22.0/kubernetes/fake) -// instead of creating and using these structs, but doing so -// requires internal knowledge of several layers of the client-go -// module, since it's not well documented (even if it were, -// you would need to touch a few different packages to get it -// working). -// -// I've (arschles) chosen to create these structs and sidestep -// the entire process, since this approach is explicit and only -// requires knowledge of the k8s.GetterWatcher interface in this -// codebase, the standard k8s/client-go package (which you -// already need to know to understand this codebase), and the -// fake watcher, which you would need to understand using either -// approach. The fake watcher documentation is linked below: -// -// (https://pkg.go.dev/k8s.io/apimachinery@v0.21.3/pkg/watch#NewFake), - -func TestStartUpdateLoop(t *testing.T) { - r := require.New(t) - a := assert.New(t) - lggr := logr.Discard() - ctx, done := context.WithCancel(context.Background()) - // ensure that we call done so that we clean - // up running test resources like the update loop, etc... - defer done() - const ( - interval = 10 * time.Millisecond - ns = "testns" - ) - - q := queue.NewFakeCounter() - table := NewTable() - r.NoError(table.AddTarget("host1", NewTarget( - "testns", - "svc1", - 8080, - "depl1", - 100, - ))) - r.NoError(table.AddTarget("host2", NewTarget( - "testns", - "svc2", - 8080, - "depl2", - 100, - ))) - - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: ConfigMapRoutingTableName, - Namespace: ns, - }, - Data: map[string]string{}, - } - r.NoError(SaveTableToConfigMap(table, cm)) - - fakeGetter := fake.NewSimpleClientset(cm) - - configMapInformer := k8s.NewInformerConfigMapUpdater( - lggr, - fakeGetter, - time.Second*1, - ns, - ) - - grp, ctx := errgroup.WithContext(ctx) - - grp.Go(func() error { - err := StartConfigMapRoutingTableUpdater( - ctx, - lggr, - configMapInformer, - ns, - table, - nil, - ) - // we purposefully cancel the context below, - // so we need to ignore that error. - if !errors.Is(err, context.Canceled) { - return err - } - return nil - }) - - // send a watch event in parallel. we'll ensure that it - // made it through in the below loop - grp.Go(func() error { - if _, err := fakeGetter. - CoreV1(). - ConfigMaps(ns). - Create(ctx, cm, metav1.CreateOptions{}); err != nil && strings.Contains( - err.Error(), - "already exists", - ) { - if err := fakeGetter. - CoreV1(). - ConfigMaps(ns). - Delete(ctx, cm.Name, metav1.DeleteOptions{}); err != nil { - return err - } - if _, err := fakeGetter. - CoreV1(). - ConfigMaps(ns). - Create(ctx, cm, metav1.CreateOptions{}); err != nil { - return err - } - } - return nil - }) - - cmGetActions := []clgotesting.Action{} - otherGetActions := []clgotesting.Action{} - const waitDur = interval * 5 - time.Sleep(waitDur) - - _, err := fakeGetter. - CoreV1(). - ConfigMaps(ns). - Get(ctx, ConfigMapRoutingTableName, metav1.GetOptions{}) - r.NoError(err) - - for _, action := range fakeGetter.Actions() { - verb := action.GetVerb() - resource := action.GetResource().Resource - // record, then ignore all actions that were not for - // ConfigMaps. - // the loop should not do anything with other resources - if resource != "configmaps" { - otherGetActions = append(otherGetActions, action) - continue - } else if verb == "get" { - cmGetActions = append(cmGetActions, action) - } - } - - // assert (don't require) these conditions so that - // we can check them, fail if necessary, but continue onward - // to check the result of the error group afterward - a.Equal( - 0, - len(otherGetActions), - "unexpected actions on non-ConfigMap resources: %s", - otherGetActions, - ) - a.Greater( - len(cmGetActions), - 0, - "no get actions for ConfigMaps", - ) - - done() - // if this test returns without timing out, - // then we can be sure that the fakeWatcher was - // able to send a watch event. if that times out - // or otherwise fails, the update loop was not properly - // listening for these events. - r.NoError(grp.Wait()) - - // the queue won't _necessarily_ have all the hosts that - // the table has in it. Hosts only show up after - // 1 or more requests have been made for it. - // check to make sure that all hosts that are in the - // queue are in the table. - table.l.RLock() - defer table.l.RUnlock() - curTable := table.m - curQCounts, err := q.Current() - r.NoError(err) - for qHost := range curQCounts.Counts { - _, ok := curTable[qHost] - r.True( - ok, - "host %s not found in table", - qHost, - ) - } -} diff --git a/pkg/routing/key.go b/pkg/routing/key.go new file mode 100644 index 000000000..68fa50d11 --- /dev/null +++ b/pkg/routing/key.go @@ -0,0 +1,82 @@ +package routing + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" +) + +type Key []byte + +func NewKey(host string, path string) Key { + if i := strings.LastIndex(host, ":"); i != -1 { + host = host[:i] + } + + path = strings.Trim(path, "/") + if path != "" { + path += "/" + } + + key := fmt.Sprintf("//%s/%s", host, path) + return []byte(key) +} + +func NewKeyFromURL(url *url.URL) Key { + if url == nil { + return nil + } + + return NewKey(url.Host, url.Path) +} + +func NewKeyFromRequest(req *http.Request) Key { + if req == nil { + return nil + } + + reqURL := req.URL + if reqURL == nil { + return nil + } + + keyURL := *reqURL + if reqHost := req.Host; reqHost != "" { + keyURL.Host = reqHost + } + + return NewKeyFromURL(&keyURL) +} + +var _ fmt.Stringer = (*Key)(nil) + +func (k Key) String() string { + return string(k) +} + +type Keys []Key + +func NewKeysFromHTTPSO(httpso *httpv1alpha1.HTTPScaledObject) Keys { + if httpso == nil { + return nil + } + spec := httpso.Spec + + size := len(spec.Hosts) + keys := make([]Key, size) + for i := 0; i < size; i++ { + host := spec.Hosts[i] + + // TODO(pedrotorres): delete this when we support path prefix + path := "" + // TODO(pedrotorres): uncomment this when we support path prefix + // path := spec.Paths[i] + + keys[i] = NewKey(host, path) + } + + return keys +} diff --git a/pkg/routing/key_test.go b/pkg/routing/key_test.go new file mode 100644 index 000000000..d6c72a68d --- /dev/null +++ b/pkg/routing/key_test.go @@ -0,0 +1,233 @@ +package routing + +import ( + "fmt" + "net/http" + "net/url" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" +) + +var _ = Describe("Key", func() { + Context("New", func() { + const ( + host0 = "kubernetes.io" + host1 = "kubernetes.io:443" + path0 = "abc/def" + path1 = "abc/def/" + path2 = "abc/def//" + path3 = "/abc/def" + path4 = "/abc/def/" + path5 = "/abc/def//" + path6 = "//abc/def" + path7 = "//abc/def/" + path8 = "//abc/def//" + norm0 = "///" + norm1 = "//kubernetes.io/" + norm2 = "///abc/def/" + norm3 = "//kubernetes.io/abc/def/" + ) + + It("returns expected key for blank host and blank path", func() { + key := NewKey("", "") + Expect(key).To(Equal(Key(norm0))) + }) + + It("returns expected key for host without port", func() { + key := NewKey(host0, "") + Expect(key).To(Equal(Key(norm1))) + }) + + It("returns expected key for host with port", func() { + key := NewKey(host1, "") + Expect(key).To(Equal(Key(norm1))) + }) + + It("returns expected key for path with no leading slashes and no trailing slashes", func() { + key := NewKey("", path0) + Expect(key).To(Equal(Key(norm2))) + }) + + It("returns expected key for path with no leading slashes and single trailing slash", func() { + key := NewKey("", path1) + Expect(key).To(Equal(Key(norm2))) + }) + + It("returns expected key for path with no leading slashes and multiple trailing slashes", func() { + key := NewKey("", path2) + Expect(key).To(Equal(Key(norm2))) + }) + + It("returns expected key for path with single leading slashes and no trailing slashes", func() { + key := NewKey("", path3) + Expect(key).To(Equal(Key(norm2))) + }) + + It("returns expected key for path with single leading slash and single trailing slash", func() { + key := NewKey("", path4) + Expect(key).To(Equal(Key(norm2))) + }) + + It("returns expected key for path with single leading slash and multiple trailing slashes", func() { + key := NewKey("", path5) + Expect(key).To(Equal(Key(norm2))) + }) + + It("returns expected key for path with multiple leading slashes and no trailing slashes", func() { + key := NewKey("", path6) + Expect(key).To(Equal(Key(norm2))) + }) + + It("returns expected key for path with multiple leading slash and single trailing slash", func() { + key := NewKey("", path7) + Expect(key).To(Equal(Key(norm2))) + }) + + It("returns expected key for path with multiple leading slash and multiple trailing slashes", func() { + key := NewKey("", path8) + Expect(key).To(Equal(Key(norm2))) + }) + + It("returns expected key for non-blank host and non-blank path", func() { + key := NewKey(host1, path8) + Expect(key).To(Equal(Key(norm3))) + }) + + It("returns nil for nil HTTPSO", func() { + key := NewKeysFromHTTPSO(nil) + Expect(key).To(BeNil()) + }) + }) + + Context("NewFromURL", func() { + It("returns expected key for URL", func() { + const ( + host = "kubernetes.io" + path = "abc/def" + norm = "//kubernetes.io/abc/def/" + ) + + url, err := url.Parse(fmt.Sprintf("https://%s:443/%s?123=456#789", host, path)) + Expect(err).NotTo(HaveOccurred()) + Expect(url).NotTo(BeNil()) + + key := NewKeyFromURL(url) + Expect(key).To(Equal(Key(norm))) + }) + + It("returns nil for nil URL", func() { + key := NewKeyFromURL(nil) + Expect(key).To(BeNil()) + }) + }) + + Context("NewFromRequest", func() { + It("returns expected key for Request", func() { + const ( + host = "kubernetes.io" + path = "abc/def" + norm = "//kubernetes.io/abc/def/" + ) + + r, err := http.NewRequest("GET", fmt.Sprintf("https://%s:443/%s?123=456#789", host, path), nil) + Expect(err).NotTo(HaveOccurred()) + Expect(r).NotTo(BeNil()) + + key := NewKeyFromRequest(r) + Expect(key).To(Equal(Key(norm))) + }) + + It("returns nil for nil Request", func() { + key := NewKeyFromRequest(nil) + Expect(key).To(BeNil()) + }) + }) + + Context("String", func() { + const ( + host = "kubernetes.io" + path = "abc/def" + norm = "//kubernetes.io/abc/def/" + ) + + It("returns expected string for key", func() { + key := NewKey(host, path) + Expect(key).NotTo(BeNil()) + Expect(key.String()).To(Equal(norm)) + }) + + It("returns expected string for key with printf", func() { + key := NewKey(host, path) + Expect(key).NotTo(BeNil()) + + str := fmt.Sprintf("%v", key) + Expect(str).To(Equal(norm)) + }) + }) +}) + +var _ = Describe("Keys", func() { + Context("New", func() { + It("returns expected key for HTTPSO", func() { + const ( + host = "kubernetes.io" + // TODO(pedrotorres): delete this when we support path prefix + norm = "//kubernetes.io/" + // TODO(pedrotorres): uncomment this when we support path prefix + // path = "abc/def" + // norm = "//kubernetes.io/abc/def/" + ) + + keys := NewKeysFromHTTPSO(&httpv1alpha1.HTTPScaledObject{ + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + host, + }, + // TODO(pedrotorres): uncomment this when we support path prefix + // PathPrefix: path, + }, + }) + Expect(keys).To(ConsistOf(Keys{ + Key(norm), + })) + }) + + It("returns expected keys for HTTPSO", func() { + const ( + host0 = "keda.sh" + host1 = "kubernetes.io" + // TODO(pedrotorres): delete this when we support path prefix + norm0 = "//keda.sh/" + norm1 = "//kubernetes.io/" + // TODO(pedrotorres): uncomment this when we support path prefix + // path0 = "abc/def" + // path1 = "123/456" + // norm0 = "//kubernetes.io/abc/def/" + // norm1 = "//keda.sh/123/456/" + ) + + keys := NewKeysFromHTTPSO(&httpv1alpha1.HTTPScaledObject{ + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + host0, + host1, + }, + // TODO(pedrotorres): uncomment this when we support path prefix + // PathPrefix: path, + }, + }) + Expect(keys).To(ConsistOf(Keys{ + Key(norm0), + Key(norm1), + })) + }) + + It("returns nil for nil HTTPSO", func() { + key := NewKeysFromHTTPSO(nil) + Expect(key).To(BeNil()) + }) + }) +}) diff --git a/pkg/routing/sharedindexinformer.go b/pkg/routing/sharedindexinformer.go new file mode 100644 index 000000000..66602ac84 --- /dev/null +++ b/pkg/routing/sharedindexinformer.go @@ -0,0 +1,10 @@ +package routing + +import ( + "k8s.io/client-go/tools/cache" +) + +type sharedIndexInformer interface { + cache.SharedIndexInformer + HasStarted() bool +} diff --git a/pkg/routing/suite_test.go b/pkg/routing/suite_test.go index 03b125de0..2cae09d1b 100644 --- a/pkg/routing/suite_test.go +++ b/pkg/routing/suite_test.go @@ -3,14 +3,8 @@ package routing import ( "testing" - kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "k8s.io/client-go/kubernetes/scheme" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" ) func TestRouting(t *testing.T) { @@ -18,37 +12,3 @@ func TestRouting(t *testing.T) { RunSpecs(t, "Routing Suite") } - -var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - - By("bootstrapping test environment") - // testEnv = &envtest.Environment{ - // CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, - // ErrorIfCRDPathMissing: true, - // } - - var err error - // cfg is defined in this file globally. - // cfg, err = testEnv.Start() - // Expect(err).NotTo(HaveOccurred()) - // Expect(cfg).NotTo(BeNil()) - - err = httpv1alpha1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - - err = kedav1alpha1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - - //+kubebuilder:scaffold:scheme - - // k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - // Expect(err).NotTo(HaveOccurred()) - // Expect(k8sClient).NotTo(BeNil()) -}) - -var _ = AfterSuite(func() { - By("tearing down the test environment") - // err := testEnv.Stop() - // Expect(err).NotTo(HaveOccurred()) -}) diff --git a/pkg/routing/table.go b/pkg/routing/table.go index 7cffc984b..a54ab167b 100644 --- a/pkg/routing/table.go +++ b/pkg/routing/table.go @@ -1,135 +1,211 @@ package routing import ( - "bytes" - "encoding/json" - "fmt" - "strings" + "context" + "net/http" "sync" + "time" + + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/cache" + + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + "github.com/kedacore/http-add-on/operator/generated/informers/externalversions" + informershttpv1alpha1 "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1" + "github.com/kedacore/http-add-on/pkg/k8s" + "github.com/kedacore/http-add-on/pkg/util" +) + +var ( + errUnknownSharedIndexInformer = errors.New("informer is not cache.sharedIndexInformer") + errStartedSharedIndexInformer = errors.New("sharedIndexInformer has started, run more than once is not allowed") + errStoppedSharedIndexInformer = errors.New("sharedIndexInformer has stopped") + errNotSyncedTable = errors.New("table has not synced") ) -type TableReader interface { - Lookup(string) (*Target, error) - Hosts() []string - HasHost(string) bool +type Table interface { + util.HealthChecker + + Start(ctx context.Context) error + Route(req *http.Request) *httpv1alpha1.HTTPScaledObject + HasSynced() bool } -type Table struct { - fmt.Stringer - m map[string]Target - l *sync.RWMutex + +type table struct { + httpScaledObjectInformer sharedIndexInformer + httpScaledObjectEventHandlerRegistration cache.ResourceEventHandlerRegistration + httpScaledObjects map[types.NamespacedName]*httpv1alpha1.HTTPScaledObject + httpScaledObjectsMutex sync.RWMutex + memoryHolder util.AtomicValue[TableMemory] + memorySignaler util.Signaler } -func NewTable() *Table { - return &Table{ - m: make(map[string]Target), - l: new(sync.RWMutex), +func NewTable(sharedInformerFactory externalversions.SharedInformerFactory, namespace string) (Table, error) { + httpScaledObjects := informershttpv1alpha1.New(sharedInformerFactory, namespace, nil).HTTPScaledObjects() + + t := table{ + httpScaledObjects: make(map[types.NamespacedName]*httpv1alpha1.HTTPScaledObject), + memorySignaler: util.NewSignaler(), } -} -// Hosts is the TableReader implementation for t. -// This function returns all hosts that are currently -// in t. -func (t Table) Hosts() []string { - t.l.RLock() - defer t.l.RUnlock() - ret := make([]string, 0, len(t.m)) - for host := range t.m { - ret = append(ret, host) - } - return ret + informer, ok := httpScaledObjects.Informer().(sharedIndexInformer) + if !ok { + return nil, errUnknownSharedIndexInformer + } + t.httpScaledObjectInformer = informer + + registration, err := informer.AddEventHandler(&t) + if err != nil { + return nil, err + } + t.httpScaledObjectEventHandlerRegistration = registration + + return &t, nil } -func (t Table) HasHost(host string) bool { - t.l.RLock() - defer t.l.RUnlock() - _, exists := t.m[host] - return exists +func (t *table) runInformer(ctx context.Context) error { + if t.httpScaledObjectInformer.HasStarted() { + return errStartedSharedIndexInformer + } + + t.httpScaledObjectInformer.Run(ctx.Done()) + + select { + case <-ctx.Done(): + return ctx.Err() + default: + return errStoppedSharedIndexInformer + } } -func (t *Table) String() string { - t.l.RLock() - defer t.l.RUnlock() - return fmt.Sprintf("%v", t.m) +func (t *table) refreshMemory(ctx context.Context) error { + // wait for event handler to be synced before first computation of routes + for !t.httpScaledObjectEventHandlerRegistration.HasSynced() { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(time.Second): + continue + } + } + + for { + m := t.newMemoryFromHTTPSOs() + t.memoryHolder.Set(m) + + if err := t.memorySignaler.Wait(ctx); err != nil { + return err + } + } } -func (t *Table) MarshalJSON() ([]byte, error) { - t.l.RLock() - defer t.l.RUnlock() - var b bytes.Buffer - err := json.NewEncoder(&b).Encode(t.m) - if err != nil { - return nil, err +func (t *table) newMemoryFromHTTPSOs() TableMemory { + t.httpScaledObjectsMutex.RLock() + defer t.httpScaledObjectsMutex.RUnlock() + + tm := NewTableMemory() + for _, newHTTPSO := range t.httpScaledObjects { + tm = tm.Remember(newHTTPSO) } - return b.Bytes(), nil + + return tm } -func (t *Table) UnmarshalJSON(data []byte) error { - t.l.Lock() - defer t.l.Unlock() - t.m = map[string]Target{} - b := bytes.NewBuffer(data) - return json.NewDecoder(b).Decode(&t.m) +var _ Table = (*table)(nil) + +func (t *table) Start(ctx context.Context) error { + eg, ctx := errgroup.WithContext(ctx) + eg.Go(util.ApplyContext(t.runInformer, ctx)) + eg.Go(util.ApplyContext(t.refreshMemory, ctx)) + return eg.Wait() } -func (t *Table) Lookup(host string) (*Target, error) { - t.l.RLock() - defer t.l.RUnlock() +func (t *table) Route(req *http.Request) *httpv1alpha1.HTTPScaledObject { + if req == nil { + return nil + } - keys := []string{host} - if i := strings.LastIndex(host, ":"); i != -1 { - keys = append(keys, host[:i]) + tm := t.memoryHolder.Get() + if tm == nil { + return nil } - for _, key := range keys { - if target, ok := t.m[key]; ok { - return &target, nil - } + key := NewKeyFromRequest(req) + return tm.Route(key) +} + +func (t *table) HasSynced() bool { + tm := t.memoryHolder.Get() + return tm != nil +} + +var _ cache.ResourceEventHandler = (*table)(nil) + +func (t *table) OnAdd(obj interface{}, _ bool) { + httpScaledObject, ok := obj.(*httpv1alpha1.HTTPScaledObject) + if !ok { + return } + key := *k8s.NamespacedNameFromObject(httpScaledObject) - return nil, ErrTargetNotFound + defer t.memorySignaler.Signal() + + t.httpScaledObjectsMutex.Lock() + defer t.httpScaledObjectsMutex.Unlock() + + t.httpScaledObjects[key] = httpScaledObject } -// AddTarget registers target for host in the routing table t -// if it didn't already exist. -// -// returns a non-nil error if it did already exist -func (t *Table) AddTarget( - host string, - target Target, -) error { - t.l.Lock() - defer t.l.Unlock() - _, ok := t.m[host] - if ok { - return fmt.Errorf( - "host %s is already registered in the routing table", - host, - ) - } - t.m[host] = target - return nil +func (t *table) OnUpdate(oldObj interface{}, newObj interface{}) { + oldHTTPSO, ok := oldObj.(*httpv1alpha1.HTTPScaledObject) + if !ok { + return + } + oldKey := *k8s.NamespacedNameFromObject(oldHTTPSO) + + newHTTPSO, ok := newObj.(*httpv1alpha1.HTTPScaledObject) + if !ok { + return + } + newKey := *k8s.NamespacedNameFromObject(newHTTPSO) + + mustDelete := oldKey != newKey + + defer t.memorySignaler.Signal() + + t.httpScaledObjectsMutex.Lock() + defer t.httpScaledObjectsMutex.Unlock() + + t.httpScaledObjects[newKey] = newHTTPSO + + if mustDelete { + delete(t.httpScaledObjects, oldKey) + } } -// RemoveTarget removes host, if it exists, and its corresponding Target entry in -// the routing table. If it does not exist, returns a non-nil error -func (t *Table) RemoveTarget(host string) error { - t.l.Lock() - defer t.l.Unlock() - _, ok := t.m[host] +func (t *table) OnDelete(obj interface{}) { + httpScaledObject, ok := obj.(*httpv1alpha1.HTTPScaledObject) if !ok { - return fmt.Errorf("host %s did not exist in the routing table", host) + return } - delete(t.m, host) - return nil + key := *k8s.NamespacedNameFromObject(httpScaledObject) + + defer t.memorySignaler.Signal() + + t.httpScaledObjectsMutex.Lock() + defer t.httpScaledObjectsMutex.Unlock() + + delete(t.httpScaledObjects, key) } -// Replace replaces t's routing table with newTable's. -// -// This function is concurrency safe for t, but not for newTable. -// The caller must ensure that no other goroutine is writing to -// newTable at the time at which they call this function. -func (t *Table) Replace(newTable *Table) { - t.l.Lock() - defer t.l.Unlock() - t.m = newTable.m +var _ util.HealthChecker = (*table)(nil) + +func (t *table) HealthCheck(_ context.Context) error { + if !t.HasSynced() { + return errNotSyncedTable + } + + return nil } diff --git a/pkg/routing/table_rpc.go b/pkg/routing/table_rpc.go deleted file mode 100644 index 4afb0c37c..000000000 --- a/pkg/routing/table_rpc.go +++ /dev/null @@ -1,93 +0,0 @@ -package routing - -import ( - "encoding/json" - "net/http" - - "github.com/go-logr/logr" - - "github.com/kedacore/http-add-on/pkg/k8s" - "github.com/kedacore/http-add-on/pkg/queue" -) - -const ( - routingPingPath = "/routing_ping" - routingFetchPath = "/routing_table" -) - -// AddFetchRoute adds a route to mux that fetches the current state of table, -// encodes it as JSON, and returns it to the HTTP client -func AddFetchRoute( - lggr logr.Logger, - mux *http.ServeMux, - table *Table, -) { - lggr = lggr.WithName("pkg.routing.AddFetchRoute") - lggr.Info("adding routing ping route", "path", routingPingPath) - mux.Handle(routingFetchPath, newTableHandler(lggr, table)) -} - -// AddPingRoute adds a route to mux that will accept an empty GET request, -// fetch the current state of the routing table from the standard routing -// table ConfigMap (ConfigMapRoutingTableName), save it to local memory, and -// return the contents of the routing table to the client. -func AddPingRoute( - lggr logr.Logger, - mux *http.ServeMux, - getter k8s.ConfigMapGetter, - table *Table, - q queue.Counter, -) { - lggr = lggr.WithName("pkg.routing.AddPingRoute") - lggr.Info("adding interceptor routing ping route", "path", routingPingPath) - mux.HandleFunc(routingPingPath, func(w http.ResponseWriter, r *http.Request) { - err := GetTable( - r.Context(), - lggr, - getter, - table, - q, - ) - if err != nil { - lggr.Error(err, "fetching new routing table") - w.WriteHeader(500) - if _, err := w.Write([]byte( - "error fetching routing table", - )); err != nil { - lggr.Error( - err, - "could not write error response to client", - ) - } - return - } - w.WriteHeader(200) - if err := json.NewEncoder(w).Encode(table); err != nil { - w.WriteHeader(500) - lggr.Error(err, "writing new routing table to the client") - return - } - }) -} - -func newTableHandler( - lggr logr.Logger, - table *Table, -) http.Handler { - lggr = lggr.WithName("pkg.routing.TableHandler") - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := json.NewEncoder(w).Encode(table) - if err != nil { - w.WriteHeader(500) - lggr.Error(err, "encoding logging table JSON") - if _, err := w.Write([]byte( - "error encoding and transmitting the routing table", - )); err != nil { - lggr.Error( - err, - "could not send error message to client", - ) - } - } - }) -} diff --git a/pkg/routing/table_rpc_test.go b/pkg/routing/table_rpc_test.go deleted file mode 100644 index 6297ff00d..000000000 --- a/pkg/routing/table_rpc_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package routing - -import ( - "context" - "testing" - - "github.com/go-logr/logr" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" - - "github.com/kedacore/http-add-on/pkg/queue" -) - -func newTableFromMap(r *require.Assertions, m map[string]Target) *Table { - table := NewTable() - for host, target := range m { - r.NoError(table.AddTarget(host, target)) - } - return table -} - -func TestRPCIntegration(t *testing.T) { - const ns = "testns" - ctx := context.Background() - lggr := logr.Discard() - r := require.New(t) - - // fetch an empty table - retTable := NewTable() - k8sCl, err := fakeConfigMapClientForTable( - NewTable(), - ns, - ConfigMapRoutingTableName, - ) - r.NoError(err) - r.NoError(GetTable( - ctx, - lggr, - k8sCl.CoreV1().ConfigMaps("testns"), - retTable, - queue.NewFakeCounter(), - )) - r.Equal(0, len(retTable.m)) - - // fetch a table with lots of targets in it - targetMap := map[string]Target{ - "host1": { - Service: "svc1", - Port: 1234, - Deployment: "depl1", - }, - "host2": { - Service: "svc2", - Port: 2345, - Deployment: "depl2", - }, - } - - retTable = NewTable() - k8sCl, err = fakeConfigMapClientForTable( - newTableFromMap(r, targetMap), - ns, - ConfigMapRoutingTableName, - ) - r.NoError(err) - r.NoError(GetTable( - ctx, - lggr, - k8sCl.CoreV1().ConfigMaps("testns"), - retTable, - queue.NewFakeCounter(), - )) - r.Equal(len(targetMap), len(retTable.m)) - r.Equal(targetMap, retTable.m) -} - -func fakeConfigMapClientForTable(t *Table, ns, name string) (*fake.Clientset, error) { - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: ns, - }, - Data: map[string]string{}, - } - if err := SaveTableToConfigMap(t, cm); err != nil { - return nil, err - } - - return fake.NewSimpleClientset(cm), nil -} diff --git a/pkg/routing/table_test.go b/pkg/routing/table_test.go index 4e202d089..835e48df6 100644 --- a/pkg/routing/table_test.go +++ b/pkg/routing/table_test.go @@ -1,216 +1,303 @@ package routing import ( - "encoding/json" - "math" - "math/rand" - "strconv" - "testing" + "context" + "time" + "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/utils/pointer" + + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + clientsetmock "github.com/kedacore/http-add-on/operator/generated/clientset/versioned/mock" + clientsethttpv1alpha1mock "github.com/kedacore/http-add-on/operator/generated/clientset/versioned/typed/http/v1alpha1/mock" + informersexternalversions "github.com/kedacore/http-add-on/operator/generated/informers/externalversions" + "github.com/kedacore/http-add-on/pkg/k8s" + "github.com/kedacore/http-add-on/pkg/util" ) -func TestTableJSONRoundTrip(t *testing.T) { +var _ = Describe("Table", func() { const ( - host = "testhost" - ns = "testns" + namespace = "default" ) - r := require.New(t) - tbl := NewTable() - tgt := NewTarget( - ns, - "testsvc", - 8082, - "testdepl", - 1234, + + var ( + ctrl *gomock.Controller + watcher *watch.FakeWatcher + httpsoCl *clientsethttpv1alpha1mock.MockHTTPScaledObjectInterface + sharedInformerFactory informersexternalversions.SharedInformerFactory + + ctx = context.Background() + httpsoList = httpv1alpha1.HTTPScaledObjectList{ + Items: []httpv1alpha1.HTTPScaledObject{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "keda-sh", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "keda.sh", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "kubernetes-io", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "kubernetes.io", + }, + TargetPendingRequests: pointer.Int32(1), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "github-com", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "github.com", + }, + Replicas: &httpv1alpha1.ReplicaStruct{ + Min: pointer.Int32(3), + }, + }, + }, + }, + } ) - r.NoError(tbl.AddTarget(host, tgt)) - b, err := json.Marshal(&tbl) - r.NoError(err) + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) - returnTbl := NewTable() - r.NoError(json.Unmarshal(b, returnTbl)) - retTarget, err := returnTbl.Lookup(host) - r.NoError(err) - r.Equal(tgt.Service, retTarget.Service) - r.Equal(tgt.Port, retTarget.Port) - r.Equal(tgt.Deployment, retTarget.Deployment) -} + watcher = watch.NewFake() -func TestTableRemove(t *testing.T) { - const ( - host = "testrm" - ns = "testns" - ) + httpsoCl = clientsethttpv1alpha1mock.NewMockHTTPScaledObjectInterface(ctrl) + httpsoCl.EXPECT(). + List(gomock.Any(), gomock.Any()). + Return(&httpsoList, nil). + AnyTimes() + httpsoCl.EXPECT(). + Watch(gomock.Any(), gomock.Any()). + Return(watcher, nil). + AnyTimes() - r := require.New(t) - tgt := NewTarget( - ns, - "testrm", - 8084, - "testrmdepl", - 1234, - ) + httpv1alpha1 := clientsethttpv1alpha1mock.NewMockHttpV1alpha1Interface(ctrl) + httpv1alpha1.EXPECT(). + HTTPScaledObjects(namespace). + Return(httpsoCl). + AnyTimes() - tbl := NewTable() - - // add the target to the table and ensure that you can look it up - r.NoError(tbl.AddTarget(host, tgt)) - retTgt, err := tbl.Lookup(host) - r.Equal(&tgt, retTgt) - r.NoError(err) - - // remove the target and ensure that you can't look it up - r.NoError(tbl.RemoveTarget(host)) - retTgt, err = tbl.Lookup(host) - r.Equal((*Target)(nil), retTgt) - r.Equal(ErrTargetNotFound, err) -} - -func TestTableReplace(t *testing.T) { - const ns = "testns" - r := require.New(t) - const host1 = "testreplhost1" - const host2 = "testreplhost2" - tgt1 := NewTarget( - ns, - "tgt1", - 9090, - "depl1", - 1234, - ) - tgt2 := NewTarget( - ns, - "tgt2", - 9091, - "depl2", - 1234, - ) - // create two routing tables, each with different targets - tbl1 := NewTable() - r.NoError(tbl1.AddTarget(host1, tgt1)) - tbl2 := NewTable() - r.NoError(tbl2.AddTarget(host2, tgt2)) + clientset := clientsetmock.NewMockInterface(ctrl) + clientset.EXPECT(). + HttpV1alpha1(). + Return(httpv1alpha1). + AnyTimes() - // replace the second table with the first and ensure that the tables - // are now equal - tbl2.Replace(tbl1) + sharedInformerFactory = informersexternalversions.NewSharedInformerFactory(clientset, 0) + }) + + AfterEach(func() { + ctrl.Finish() + }) - r.Equal(tbl1, tbl2) -} + Context("New", func() { + It("returns a table with fields initialized", func() { + i, err := NewTable(sharedInformerFactory, namespace) + Expect(err).NotTo(HaveOccurred()) + Expect(i).NotTo(BeNil()) -var _ = Describe("Table", func() { - Describe("Lookup", func() { + t, ok := i.(*table) + Expect(ok).To(BeTrue()) + Expect(t.httpScaledObjectInformer).NotTo(BeNil()) + Expect(t.httpScaledObjectEventHandlerRegistration).NotTo(BeNil()) + Expect(t.httpScaledObjects).NotTo(BeNil()) + Expect(t.memorySignaler).NotTo(BeNil()) + + // TODO(pedrotorres): mock to check registration + // TODO(pedrotorres): refactor to check namespace + }) + + // TODO(pedrotorres): test code path where informer is not sharedIndexInformer + // TODO(pedrotorres): test code path where informer#AddEventHandler fails + }) + + Context("runInformer", func() { var ( - tltcs = newTableLookupTestCases(5) - table = NewTable() + t *table ) - Context("with new port-agnostic configuration", func() { - BeforeEach(func() { - for _, tltc := range tltcs { - err := table.AddTarget(tltc.HostWithoutPort(), tltc.Target()) - Expect(err).NotTo(HaveOccurred()) - } - }) - - AfterEach(func() { - for _, tltc := range tltcs { - err := table.RemoveTarget(tltc.HostWithoutPort()) - Expect(err).NotTo(HaveOccurred()) - } - }) - - It("should return correct target for host without port", func() { - for _, tltc := range tltcs { - target, err := table.Lookup(tltc.HostWithoutPort()) - Expect(err).NotTo(HaveOccurred()) - Expect(target).To(HaveValue(Equal(tltc.Target()))) - } - }) - - It("should return correct target for host with port", func() { - for _, tltc := range tltcs { - target, err := table.Lookup(tltc.HostWithPort()) - Expect(err).NotTo(HaveOccurred()) - Expect(target).To(HaveValue(Equal(tltc.Target()))) - } - }) + BeforeEach(func() { + i, _ := NewTable(sharedInformerFactory, namespace) + t = i.(*table) }) - Context("with legacy port-specific configuration", func() { - BeforeEach(func() { - for _, tltc := range tltcs { - err := table.AddTarget(tltc.HostWithPort(), tltc.Target()) - Expect(err).NotTo(HaveOccurred()) - } - }) - - AfterEach(func() { - for _, tltc := range tltcs { - err := table.RemoveTarget(tltc.HostWithPort()) - Expect(err).NotTo(HaveOccurred()) - } - }) - - It("should error for host without port", func() { - for _, tltc := range tltcs { - target, err := table.Lookup(tltc.HostWithoutPort()) - Expect(err).To(MatchError(ErrTargetNotFound)) - Expect(target).To(BeNil()) - } - }) - - It("should return correct target for host with port", func() { - for _, tltc := range tltcs { - target, err := table.Lookup(tltc.HostWithPort()) - Expect(err).NotTo(HaveOccurred()) - Expect(target).To(HaveValue(Equal(tltc.Target()))) - } - }) + It("starts shared informer factory", func() { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + go util.IgnoringError(util.ApplyContext(t.runInformer, ctx)) + + time.Sleep(time.Second) + + b := t.httpScaledObjectInformer.HasStarted() + Expect(b).To(BeTrue()) }) + + It("returns when context is done", func() { + ctx, cancel := context.WithCancel(ctx) + cancel() + + err := util.WithTimeout(time.Second, util.ApplyContext(t.runInformer, ctx)) + Expect(err).To(MatchError(context.Canceled)) + }) + + It("returns when informer has already started", func() { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + go t.httpScaledObjectInformer.Run(ctx.Done()) + + time.Sleep(time.Second) + + err := util.WithTimeout(time.Second, util.ApplyContext(t.runInformer, ctx)) + Expect(err).To(MatchError(errStartedSharedIndexInformer)) + }) + + // TODO(pedrotorres): test code path where informer stops }) -}) -type tableLookupTestCase struct { - target Target -} - -func newTableLookupTestCase() tableLookupTestCase { - target := NewTarget( - strconv.Itoa(rand.Int()), - strconv.Itoa(rand.Int()), - rand.Intn(math.MaxUint16), - strconv.Itoa(rand.Int()), - int32(rand.Intn(math.MaxUint8)), - ) + Context("refreshMemory", func() { + var ( + t *table + ) - return tableLookupTestCase{ - target: target, - } -} + BeforeEach(func() { + i, _ := NewTable(sharedInformerFactory, namespace) + t = i.(*table) + }) + + It("refreshes memory on first iteration", func() { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + for _, httpso := range httpsoList.Items { + httpso := httpso + + key := *k8s.NamespacedNameFromObject(&httpso) + t.httpScaledObjects[key] = &httpso + } + + go util.IgnoringError(util.ApplyContext(t.runInformer, ctx)) + go util.IgnoringError(util.ApplyContext(t.refreshMemory, ctx)) + + time.Sleep(2 * time.Second) + + tm := t.memoryHolder.Get() + Expect(tm).NotTo(BeNil()) + + for _, httpso := range httpsoList.Items { + namespacedName := k8s.NamespacedNameFromObject(&httpso) + ret := tm.Recall(namespacedName) + Expect(ret).To(Equal(&httpso)) + } + }) + + It("refreshes memory after signal", func() { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + for _, httpso := range httpsoList.Items { + httpso := httpso + + key := *k8s.NamespacedNameFromObject(&httpso) + t.httpScaledObjects[key] = &httpso + } + + go util.IgnoringError(util.ApplyContext(t.runInformer, ctx)) + go util.IgnoringError(util.ApplyContext(t.refreshMemory, ctx)) + + time.Sleep(2 * time.Second) + + httpso := httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "azure-com", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "azure.com", + }, + Replicas: &httpv1alpha1.ReplicaStruct{ + Min: pointer.Int32(3), + }, + }, + } + t.httpScaledObjects[*k8s.NamespacedNameFromObject(&httpso)] = &httpso + + first := httpsoList.Items[0] + delete(t.httpScaledObjects, *k8s.NamespacedNameFromObject(&first)) + + t.memorySignaler.Signal() -func (tltc tableLookupTestCase) Target() Target { - return tltc.target -} + time.Sleep(time.Second) -func (tltc tableLookupTestCase) HostWithoutPort() string { - return tltc.target.Service + "." + tltc.target.Namespace + ".svc.cluster.local" -} + tm := t.memoryHolder.Get() + Expect(tm).NotTo(BeNil()) -func (tltc tableLookupTestCase) HostWithPort() string { - return tltc.HostWithoutPort() + ":" + strconv.Itoa(tltc.target.Port) -} + for _, httpso := range append(httpsoList.Items[1:], httpso) { + namespacedName := k8s.NamespacedNameFromObject(&httpso) + ret := tm.Recall(namespacedName) + Expect(ret).To(Equal(&httpso)) + } -type tableLookupTestCases []tableLookupTestCase + namespacedName := k8s.NamespacedNameFromObject(&first) + ret := tm.Recall(namespacedName) + Expect(ret).To(BeNil()) + }) + + It("returns when context is done", func() { + ctx, cancel := context.WithCancel(ctx) + cancel() + + err := util.WithTimeout(time.Second, util.ApplyContext(t.refreshMemory, ctx)) + Expect(err).To(MatchError(context.Canceled)) + }) + }) -func newTableLookupTestCases(count uint) tableLookupTestCases { - tltcs := make(tableLookupTestCases, count) - for i := uint(0); i < count; i++ { - tltcs[i] = newTableLookupTestCase() - } - return tltcs -} + Context("newMemoryFromHTTPSOs", func() { + var ( + t *table + ) + + BeforeEach(func() { + i, _ := NewTable(sharedInformerFactory, namespace) + t = i.(*table) + }) + + It("returns new memory based on HTTPSOs", func() { + for _, httpso := range httpsoList.Items { + httpso := httpso + + key := *k8s.NamespacedNameFromObject(&httpso) + t.httpScaledObjects[key] = &httpso + } + + tm := t.newMemoryFromHTTPSOs() + + for _, httpso := range httpsoList.Items { + namespacedName := k8s.NamespacedNameFromObject(&httpso) + + ret := tm.Recall(namespacedName) + Expect(ret).To(Equal(&httpso)) + } + }) + }) +}) diff --git a/pkg/routing/tablememory.go b/pkg/routing/tablememory.go new file mode 100644 index 000000000..0cb0d8f10 --- /dev/null +++ b/pkg/routing/tablememory.go @@ -0,0 +1,126 @@ +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" + "github.com/kedacore/http-add-on/pkg/k8s" +) + +type TableMemory interface { + Remember(httpso *httpv1alpha1.HTTPScaledObject) TableMemory + Recall(namespacedName *types.NamespacedName) *httpv1alpha1.HTTPScaledObject + Forget(namespacedName *types.NamespacedName) TableMemory + Route(key Key) *httpv1alpha1.HTTPScaledObject +} + +type tableMemory struct { + index *iradix.Tree[*httpv1alpha1.HTTPScaledObject] + store *iradix.Tree[*httpv1alpha1.HTTPScaledObject] +} + +func NewTableMemory() TableMemory { + return tableMemory{ + index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + } +} + +var _ TableMemory = (*tableMemory)(nil) + +func (tm tableMemory) Remember(httpso *httpv1alpha1.HTTPScaledObject) TableMemory { + if httpso == nil { + return tm + } + httpso = httpso.DeepCopy() + + indexKey := newTableMemoryIndexKeyFromHTTPSO(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 + } + + return tableMemory{ + index: index, + store: store, + } +} + +func (tm tableMemory) Recall(namespacedName *types.NamespacedName) *httpv1alpha1.HTTPScaledObject { + if namespacedName == nil { + return nil + } + + indexKey := newTableMemoryIndexKey(namespacedName) + httpso, _ := tm.index.Get(indexKey) + if httpso == nil { + return nil + } + + return httpso.DeepCopy() +} + +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 { + 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 + } + + return tableMemory{ + index: index, + store: store, + } +} + +func (tm tableMemory) Route(key Key) *httpv1alpha1.HTTPScaledObject { + _, httpso, _ := tm.store.Root().LongestPrefix(key) + return httpso +} + +type tableMemoryIndexKey []byte + +func newTableMemoryIndexKey(namespacedName *types.NamespacedName) tableMemoryIndexKey { + if namespacedName == nil { + return nil + } + + return []byte(namespacedName.String()) +} + +func newTableMemoryIndexKeyFromHTTPSO(httpso *httpv1alpha1.HTTPScaledObject) tableMemoryIndexKey { + if httpso == nil { + return nil + } + + namespacedName := k8s.NamespacedNameFromObject(httpso) + return newTableMemoryIndexKey(namespacedName) +} diff --git a/pkg/routing/tablememory_test.go b/pkg/routing/tablememory_test.go new file mode 100644 index 000000000..204cbfe05 --- /dev/null +++ b/pkg/routing/tablememory_test.go @@ -0,0 +1,571 @@ +package routing + +import ( + "fmt" + "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" + "k8s.io/utils/pointer" + + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + "github.com/kedacore/http-add-on/pkg/k8s" +) + +var _ = Describe("TableMemory", func() { + const ( + nameSuffix = "-br" + hostSuffix = ".br" + ) + + var ( + httpso0 = httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keda-sh", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "keda.sh", + }, + }, + } + + httpso0NamespacedName = *k8s.NamespacedNameFromObject(&httpso0) + + httpso1 = httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "one-one-one-one", + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "1.1.1.1", + }, + }, + } + + httpso1NamespacedName = *k8s.NamespacedNameFromObject(&httpso1) + + // TODO(pedrotorres): uncomment this when we support path prefix + // httpsoList = httpv1alpha1.HTTPScaledObjectList{ + // Items: []httpv1alpha1.HTTPScaledObject{ + // { + // ObjectMeta: metav1.ObjectMeta{ + // Name: "/", + // }, + // Spec: httpv1alpha1.HTTPScaledObjectSpec{ + // Host: "localhost", + // PathPrefix: "/", + // }, + // }, + // { + // ObjectMeta: metav1.ObjectMeta{ + // Name: "/f", + // }, + // Spec: httpv1alpha1.HTTPScaledObjectSpec{ + // Host: "localhost", + // PathPrefix: "/f", + // }, + // }, + // { + // ObjectMeta: metav1.ObjectMeta{ + // Name: "fo", + // }, + // Spec: httpv1alpha1.HTTPScaledObjectSpec{ + // Host: "localhost", + // PathPrefix: "fo", + // }, + // }, + // { + // ObjectMeta: metav1.ObjectMeta{ + // Name: "foo/", + // }, + // Spec: httpv1alpha1.HTTPScaledObjectSpec{ + // Host: "localhost", + // PathPrefix: "foo/", + // }, + // }, + // }, + // } + + assertIndex = func(tm tableMemory, input *httpv1alpha1.HTTPScaledObject, expected *httpv1alpha1.HTTPScaledObject) { + okMatcher := BeTrue() + if expected == nil { + okMatcher = BeFalse() + } + + httpsoMatcher := Equal(expected) + if expected == nil { + httpsoMatcher = BeNil() + } + + namespacedName := k8s.NamespacedNameFromObject(input) + indexKey := newTableMemoryIndexKey(namespacedName) + httpso, ok := tm.index.Get(indexKey) + Expect(ok).To(okMatcher) + Expect(httpso).To(httpsoMatcher) + } + + assertStore = func(tm tableMemory, input *httpv1alpha1.HTTPScaledObject, expected *httpv1alpha1.HTTPScaledObject) { + okMatcher := BeTrue() + if expected == nil { + okMatcher = BeFalse() + } + + httpsoMatcher := Equal(expected) + if expected == nil { + httpsoMatcher = BeNil() + } + + storeKeys := NewKeysFromHTTPSO(input) + for _, storeKey := range storeKeys { + httpso, ok := tm.store.Get(storeKey) + Expect(ok).To(okMatcher) + Expect(httpso).To(httpsoMatcher) + } + } + + assertTrees = func(tm tableMemory, input *httpv1alpha1.HTTPScaledObject, expected *httpv1alpha1.HTTPScaledObject) { + assertIndex(tm, input, expected) + assertStore(tm, input, expected) + } + + insertIndex = func(tm tableMemory, httpso *httpv1alpha1.HTTPScaledObject) tableMemory { + namespacedName := k8s.NamespacedNameFromObject(httpso) + indexKey := newTableMemoryIndexKey(namespacedName) + tm.index, _, _ = tm.index.Insert(indexKey, httpso) + + return tm + } + + insertStore = func(tm tableMemory, httpso *httpv1alpha1.HTTPScaledObject) tableMemory { + storeKeys := NewKeysFromHTTPSO(httpso) + for _, storeKey := range storeKeys { + tm.store, _, _ = tm.store.Insert(storeKey, httpso) + } + + return tm + } + + insertTrees = func(tm tableMemory, httpso *httpv1alpha1.HTTPScaledObject) tableMemory { + tm = insertIndex(tm, httpso) + tm = insertStore(tm, httpso) + return tm + } + ) + + Context("New", func() { + It("returns a tableMemory with initialized tree", func() { + i := NewTableMemory() + + tm, ok := i.(tableMemory) + Expect(ok).To(BeTrue()) + Expect(tm.index).NotTo(BeNil()) + Expect(tm.store).NotTo(BeNil()) + }) + }) + + 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 = 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 = tm.Remember(&httpso0).(tableMemory) + tm = tm.Remember(&httpso1).(tableMemory) + + assertTrees(tm, &httpso1, &httpso1) + assertTrees(tm, &httpso0, &httpso0) + }) + + 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 = tm.Remember(&httpso0).(tableMemory) + + httpso1 := *httpso0.DeepCopy() + httpso1.Spec.TargetPendingRequests = pointer.Int32(1) + tm = tm.Remember(&httpso1).(tableMemory) + + assertTrees(tm, &httpso0, &httpso1) + }) + + 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 = tm.Remember(&httpso0).(tableMemory) + tm = tm.Remember(&httpso1).(tableMemory) + + httpso2 := *httpso1.DeepCopy() + httpso2.Spec.TargetPendingRequests = pointer.Int32(1) + tm = tm.Remember(&httpso2).(tableMemory) + + assertTrees(tm, &httpso1, &httpso2) + assertTrees(tm, &httpso0, &httpso0) + }) + + It("returns a tableMemory with deep-copied object", func() { + tm := tableMemory{ + index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + } + + httpso := *httpso0.DeepCopy() + tm = tm.Remember(&httpso).(tableMemory) + + httpso.Spec.Hosts[0] += hostSuffix + assertTrees(tm, &httpso0, &httpso0) + }) + + It("gives precedence to the oldest object on conflict", func() { + tm := tableMemory{ + index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + } + + t0 := time.Now() + + httpso00 := *httpso0.DeepCopy() + httpso00.ObjectMeta.CreationTimestamp = metav1.NewTime(t0) + tm = tm.Remember(&httpso00).(tableMemory) + + httpso01 := *httpso0.DeepCopy() + httpso01.ObjectMeta.Name += nameSuffix + httpso01.ObjectMeta.CreationTimestamp = metav1.NewTime(t0.Add(-time.Minute)) + tm = tm.Remember(&httpso01).(tableMemory) + + httpso10 := *httpso1.DeepCopy() + httpso10.ObjectMeta.CreationTimestamp = metav1.NewTime(t0) + tm = tm.Remember(&httpso10).(tableMemory) + + httpso11 := *httpso1.DeepCopy() + httpso11.ObjectMeta.Name += nameSuffix + httpso11.ObjectMeta.CreationTimestamp = metav1.NewTime(t0.Add(+time.Minute)) + tm = tm.Remember(&httpso11).(tableMemory) + + assertIndex(tm, &httpso00, &httpso00) + assertStore(tm, &httpso00, &httpso01) + + assertIndex(tm, &httpso01, &httpso01) + assertStore(tm, &httpso01, &httpso01) + + assertIndex(tm, &httpso10, &httpso10) + assertStore(tm, &httpso10, &httpso10) + + assertIndex(tm, &httpso11, &httpso11) + assertStore(tm, &httpso11, &httpso10) + }) + }) + + 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 = insertTrees(tm, &httpso0) + + tm = tm.Forget(&httpso0NamespacedName).(tableMemory) + + assertTrees(tm, &httpso0, nil) + }) + + 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 = insertTrees(tm, &httpso0) + tm = insertTrees(tm, &httpso1) + + tm = tm.Forget(&httpso0NamespacedName).(tableMemory) + + assertTrees(tm, &httpso1, &httpso1) + assertTrees(tm, &httpso0, nil) + }) + + It("returns unchanged tableMemory when object is absent", func() { + tm := tableMemory{ + index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + } + tm = insertTrees(tm, &httpso0) + + index0 := *tm.index + store0 := *tm.store + tm = tm.Forget(&httpso1NamespacedName).(tableMemory) + index1 := *tm.index + store1 := *tm.store + Expect(index1).To(Equal(index0)) + Expect(store1).To(Equal(store0)) + }) + + It("forgets only when namespaced names match on conflict", func() { + tm := tableMemory{ + index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + } + + t0 := time.Now() + + httpso00 := *httpso0.DeepCopy() + httpso00.ObjectMeta.CreationTimestamp = metav1.NewTime(t0) + tm = insertTrees(tm, &httpso00) + + httpso01 := *httpso0.DeepCopy() + httpso01.ObjectMeta.Name += nameSuffix + httpso01.ObjectMeta.CreationTimestamp = metav1.NewTime(t0.Add(-time.Minute)) + tm = insertTrees(tm, &httpso01) + + httpso10 := *httpso1.DeepCopy() + httpso10.ObjectMeta.Name += nameSuffix + httpso10.ObjectMeta.CreationTimestamp = metav1.NewTime(t0) + tm = insertTrees(tm, &httpso10) + + httpso11 := *httpso1.DeepCopy() + httpso11.ObjectMeta.CreationTimestamp = metav1.NewTime(t0.Add(-time.Minute)) + tm = insertTrees(tm, &httpso11) + + tm = tm.Forget(&httpso0NamespacedName).(tableMemory) + tm = tm.Forget(&httpso1NamespacedName).(tableMemory) + + assertIndex(tm, &httpso00, nil) + assertStore(tm, &httpso00, &httpso01) + + assertIndex(tm, &httpso01, &httpso01) + assertStore(tm, &httpso01, &httpso01) + + assertIndex(tm, &httpso10, &httpso10) + assertStore(tm, &httpso10, nil) + + assertIndex(tm, &httpso11, nil) + assertStore(tm, &httpso11, nil) + }) + }) + + Context("Recall", func() { + It("returns object with matching key", func() { + tm := tableMemory{ + index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + } + tm = insertTrees(tm, &httpso0) + + httpso := tm.Recall(&httpso0NamespacedName) + Expect(httpso).To(Equal(&httpso0)) + }) + + It("returns nil when object is absent", func() { + tm := tableMemory{ + index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + } + tm = insertTrees(tm, &httpso0) + + httpso := tm.Recall(&httpso1NamespacedName) + Expect(httpso).To(BeNil()) + }) + + It("returns deep-copied object", func() { + tm := tableMemory{ + index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + } + tm = insertTrees(tm, &httpso0) + + httpso := tm.Recall(&httpso0NamespacedName) + Expect(httpso).To(Equal(&httpso0)) + + httpso.Spec.Hosts[0] += hostSuffix + + assertTrees(tm, &httpso0, &httpso0) + }) + }) + + 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 = insertTrees(tm, &httpso0) + + url, err := url.Parse(fmt.Sprintf("https://%s.br", httpso0.Spec.Hosts[0])) + Expect(err).NotTo(HaveOccurred()) + Expect(url).NotTo(BeNil()) + urlKey := NewKeyFromURL(url) + Expect(urlKey).NotTo(BeNil()) + httpso := tm.Route(urlKey) + Expect(httpso).To(BeNil()) + }) + + It("returns expected object with matching host for URL", func() { + tm := tableMemory{ + index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + } + tm = insertTrees(tm, &httpso0) + tm = insertTrees(tm, &httpso1) + + //goland:noinspection HttpUrlsUsage + url0, err := url.Parse(fmt.Sprintf("http://%s", httpso0.Spec.Hosts[0])) + Expect(err).NotTo(HaveOccurred()) + Expect(url0).NotTo(BeNil()) + url0Key := NewKeyFromURL(url0) + Expect(url0Key).NotTo(BeNil()) + ret0 := tm.Route(url0Key) + Expect(ret0).To(Equal(&httpso0)) + + url1, err := url.Parse(fmt.Sprintf("https://%s:443/abc/def?123=456#789", httpso1.Spec.Hosts[0])) + Expect(err).NotTo(HaveOccurred()) + Expect(url1).NotTo(BeNil()) + url1Key := NewKeyFromURL(url1) + Expect(url1Key).NotTo(BeNil()) + ret1 := tm.Route(url1Key) + Expect(ret1).To(Equal(&httpso1)) + }) + + // TODO(pedrotorres): uncomment this when we support path prefix + // + // It("returns nil when no matching pathPrefix for URL", func() { + // var ( + // httpsoFoo = httpsoList.Items[3] + // ) + // + // tm := tableMemory{ + // index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + // store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + // } + // tm = insertTrees(tm, &httpsoFoo) + // + // //goland:noinspection HttpUrlsUsage + // url, err := url.Parse(fmt.Sprintf("http://%s/bar%s", httpsoFoo.Spec.Host, httpsoFoo.Spec.PathPrefix)) + // Expect(err).NotTo(HaveOccurred()) + // Expect(url).NotTo(BeNil()) + // urlKey := NewKeyFromURL(url) + // Expect(urlKey).NotTo(BeNil()) + // httpso := tm.Route(urlKey) + // Expect(httpso).To(BeNil()) + // }) + // + // It("returns expected object with matching pathPrefix for URL", func() { + // tm := tableMemory{ + // index: iradix.New[*httpv1alpha1.HTTPScaledObject](), + // store: iradix.New[*httpv1alpha1.HTTPScaledObject](), + // } + // for _, httpso := range httpsoList.Items { + // httpso := httpso + // + // tm = insertTrees(tm, &httpso) + // } + // + // for _, httpso := range httpsoList.Items { + // url, err := url.Parse(fmt.Sprintf("https://%s/%s", httpso.Spec.Host, httpso.Spec.PathPrefix)) + // Expect(err).NotTo(HaveOccurred()) + // Expect(url).NotTo(BeNil()) + // urlKey := NewKeyFromURL(url) + // Expect(urlKey).NotTo(BeNil()) + // ret := tm.Route(urlKey) + // Expect(ret).To(Equal(&httpso)) + // } + // + // for _, httpso := range httpsoList.Items { + // url, err := url.Parse(fmt.Sprintf("https://%s/%s/bar", httpso.Spec.Host, httpso.Spec.PathPrefix)) + // Expect(err).NotTo(HaveOccurred()) + // Expect(url).NotTo(BeNil()) + // urlKey := NewKeyFromURL(url) + // Expect(urlKey).NotTo(BeNil()) + // ret := tm.Route(urlKey) + // Expect(ret).To(Equal(&httpso)) + // } + // }) + }) + + Context("E2E", func() { + It("succeeds", func() { + tm := NewTableMemory() + + ret0 := tm.Recall(&httpso0NamespacedName) + Expect(ret0).To(BeNil()) + + tm = tm.Remember(&httpso0) + + ret1 := tm.Recall(&httpso0NamespacedName) + Expect(ret1).To(Equal(&httpso0)) + + tm = tm.Forget(&httpso0NamespacedName) + + ret2 := tm.Recall(&httpso0NamespacedName) + Expect(ret2).To(BeNil()) + + tm = tm.Remember(&httpso0) + tm = tm.Remember(&httpso1) + + ret3 := tm.Recall(&httpso0NamespacedName) + Expect(ret3).To(Equal(&httpso0)) + + ret4 := tm.Recall(&httpso1NamespacedName) + Expect(ret4).To(Equal(&httpso1)) + + //goland:noinspection HttpUrlsUsage + url0, err := url.Parse(fmt.Sprintf("http://%s:80?123=456#789", httpso0.Spec.Hosts[0])) + Expect(err).NotTo(HaveOccurred()) + Expect(url0).NotTo(BeNil()) + + url0Key := NewKeyFromURL(url0) + Expect(url0Key).NotTo(BeNil()) + + ret5 := tm.Route(url0Key) + Expect(ret5).To(Equal(&httpso0)) + + url1, err := url.Parse(fmt.Sprintf("https://user:pass@%s:443/abc/def", httpso1.Spec.Hosts[0])) + Expect(err).NotTo(HaveOccurred()) + Expect(url1).NotTo(BeNil()) + + url1Key := NewKeyFromURL(url1) + Expect(url1Key).NotTo(BeNil()) + + ret6 := tm.Route(url1Key) + Expect(ret6).To(Equal(&httpso1)) + + url2, err := url.Parse("http://0.0.0.0") + Expect(err).NotTo(HaveOccurred()) + Expect(url2).NotTo(BeNil()) + + url2Key := NewKeyFromURL(url2) + Expect(url2Key).NotTo(BeNil()) + + ret7 := tm.Route(url2Key) + Expect(ret7).To(BeNil()) + + tm = tm.Forget(&httpso0NamespacedName) + + ret8 := tm.Route(url0Key) + Expect(ret8).To(BeNil()) + + httpso := *httpso1.DeepCopy() + httpso.Spec.TargetPendingRequests = pointer.Int32(1) + + tm = tm.Remember(&httpso) + + ret9 := tm.Route(url1Key) + Expect(ret9).To(Equal(&httpso)) + }) + }) +}) diff --git a/pkg/routing/target.go b/pkg/routing/target.go deleted file mode 100644 index 49e2a48b4..000000000 --- a/pkg/routing/target.go +++ /dev/null @@ -1,59 +0,0 @@ -package routing - -import ( - "errors" - "fmt" - "net/url" -) - -// ErrTargetNotFound is returned when a target is not -// found in the table. -var ErrTargetNotFound = errors.New("Target not found") - -// Target is a single target in the routing table. -type Target struct { - Service string - Port int - Deployment string - Namespace string - TargetPendingRequests int32 -} - -// NewTarget creates a new Target from the given parameters. -func NewTarget( - namespace, - svc string, - port int, - depl string, - targetPendingReqs int32, -) Target { - return Target{ - Service: svc, - Port: port, - Deployment: depl, - TargetPendingRequests: targetPendingReqs, - Namespace: namespace, - } -} - -// ServiceURLFunc is a function that returns the full in-cluster -// URL for the given Target. -// -// ServiceURL is the production implementation of this function -type ServiceURLFunc func(Target) (*url.URL, error) - -// ServiceURL returns the full URL for the Kubernetes service, -// port and namespace of t. -func ServiceURL(t Target) (*url.URL, error) { - urlStr := fmt.Sprintf( - "http://%s.%s:%d", - t.Service, - t.Namespace, - t.Port, - ) - u, err := url.Parse(urlStr) - if err != nil { - return nil, err - } - return u, nil -} diff --git a/pkg/routing/target_test.go b/pkg/routing/target_test.go deleted file mode 100644 index ec271c7d3..000000000 --- a/pkg/routing/target_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package routing - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestTargetServiceURL(t *testing.T) { - r := require.New(t) - - target := NewTarget( - "testns", - "testsvc", - 8081, - "testdeploy", - 1234, - ) - svcURL, err := ServiceURL(target) - r.NoError(err) - r.Equal( - fmt.Sprintf("%s.%s:%d", target.Service, target.Namespace, target.Port), - svcURL.Host, - ) -} diff --git a/pkg/routing/test/table.go b/pkg/routing/test/table.go new file mode 100644 index 000000000..e0ea83aba --- /dev/null +++ b/pkg/routing/test/table.go @@ -0,0 +1,40 @@ +package test + +import ( + "context" + "net/http" + + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + "github.com/kedacore/http-add-on/pkg/routing" + "github.com/kedacore/http-add-on/pkg/util" +) + +type Table struct { + Memory map[string]*httpv1alpha1.HTTPScaledObject +} + +func NewTable() *Table { + return &Table{ + Memory: make(map[string]*httpv1alpha1.HTTPScaledObject), + } +} + +var _ routing.Table = (*Table)(nil) + +func (t Table) Start(_ context.Context) error { + return nil +} + +func (t Table) Route(req *http.Request) *httpv1alpha1.HTTPScaledObject { + return t.Memory[req.Host] +} + +func (t Table) HasSynced() bool { + return true +} + +var _ util.HealthChecker = (*Table)(nil) + +func (t Table) HealthCheck(_ context.Context) error { + return nil +} diff --git a/pkg/test/round_trip.go b/pkg/test/round_trip.go deleted file mode 100644 index 039bfdb2b..000000000 --- a/pkg/test/round_trip.go +++ /dev/null @@ -1,23 +0,0 @@ -// Package test contains helper and utility functions -// for use primarily in tests. -// -// It is generally not a good idea to use anything in this -// package in production code, unless you're very familiar -// with that code and its performance characteristics. -package test - -import "encoding/json" - -// JSONRoundTrip round trips src to JSON and back -// out into target -// -// This function is primarily intended for translating -// a map to a configuration struct, and intended for use -// in tests. -func JSONRoundTrip(src interface{}, target interface{}) error { - srcBytes, err := json.Marshal(src) - if err != nil { - return err - } - return json.Unmarshal(srcBytes, target) -} diff --git a/pkg/util/async.go b/pkg/util/async.go new file mode 100644 index 000000000..1e7c3b4d2 --- /dev/null +++ b/pkg/util/async.go @@ -0,0 +1,22 @@ +package util + +import ( + "fmt" + "time" +) + +func WithTimeout(d time.Duration, f func() error) error { + errs := make(chan error) + defer close(errs) + + go func() { + errs <- f() + }() + + select { + case err := <-errs: + return err + case <-time.After(d): + return fmt.Errorf("timed out after %v", d) + } +} diff --git a/pkg/util/atomicvalue.go b/pkg/util/atomicvalue.go new file mode 100644 index 000000000..3220929ad --- /dev/null +++ b/pkg/util/atomicvalue.go @@ -0,0 +1,28 @@ +package util + +import ( + "sync/atomic" +) + +type AtomicValue[V any] struct { + atomicValue atomic.Value +} + +func NewAtomicValue[V any](v V) *AtomicValue[V] { + var av AtomicValue[V] + av.Set(v) + + return &av +} + +func (av *AtomicValue[V]) Get() V { + if v, ok := av.atomicValue.Load().(V); ok { + return v + } + + return *new(V) +} + +func (av *AtomicValue[V]) Set(v V) { + av.atomicValue.Store(v) +} diff --git a/pkg/util/atomicvalue_test.go b/pkg/util/atomicvalue_test.go new file mode 100644 index 000000000..ab6a57b3c --- /dev/null +++ b/pkg/util/atomicvalue_test.go @@ -0,0 +1,69 @@ +package util + +import ( + "math/rand" + "sync/atomic" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("AtomicValue", func() { + var ( + i int + ) + + BeforeEach(func() { + i = rand.Int() + }) + + Context("New", func() { + It("returns an AtomicValue with the expected value", func() { + v := NewAtomicValue[int](i) + + out, ok := v.atomicValue.Load().(int) + Expect(ok).To(BeTrue()) + Expect(out).To(Equal(i)) + }) + }) + + Context("Get", func() { + It("returns the stored value", func() { + var a atomic.Value + a.Store(i) + + v := AtomicValue[int]{ + atomicValue: a, + } + + out := v.Get() + Expect(out).To(Equal(i)) + }) + }) + + Context("Set", func() { + It("stores the expected value", func() { + var v AtomicValue[int] + v.Set(i) + + out, ok := v.atomicValue.Load().(int) + Expect(ok).To(BeTrue()) + Expect(out).To(Equal(i)) + }) + }) + + Context("E2E", func() { + It("succeeds", func() { + v := NewAtomicValue[int](i) + + out0 := v.Get() + Expect(out0).To(Equal(i)) + + i = rand.Int() + v.Set(i) + + out1 := v.Get() + Expect(out1).To(Equal(i)) + }) + }) +}) diff --git a/pkg/util/context.go b/pkg/util/context.go new file mode 100644 index 000000000..8e30dad75 --- /dev/null +++ b/pkg/util/context.go @@ -0,0 +1,45 @@ +package util + +import ( + "context" + "net/url" + + "github.com/go-logr/logr" + + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" +) + +type contextKey int + +const ( + ckLogger contextKey = iota + ckHTTPSO + ckStream +) + +func ContextWithLogger(ctx context.Context, logger logr.Logger) context.Context { + return context.WithValue(ctx, ckLogger, logger) +} + +func LoggerFromContext(ctx context.Context) logr.Logger { + cv, _ := ctx.Value(ckLogger).(logr.Logger) + return cv +} + +func ContextWithHTTPSO(ctx context.Context, httpso *httpv1alpha1.HTTPScaledObject) context.Context { + return context.WithValue(ctx, ckHTTPSO, httpso) +} + +func HTTPSOFromContext(ctx context.Context) *httpv1alpha1.HTTPScaledObject { + cv, _ := ctx.Value(ckHTTPSO).(*httpv1alpha1.HTTPScaledObject) + return cv +} + +func ContextWithStream(ctx context.Context, url *url.URL) context.Context { + return context.WithValue(ctx, ckStream, url) +} + +func StreamFromContext(ctx context.Context) *url.URL { + cv, _ := ctx.Value(ckStream).(*url.URL) + return cv +} diff --git a/pkg/util/contexthttp.go b/pkg/util/contexthttp.go new file mode 100644 index 000000000..d95ad0174 --- /dev/null +++ b/pkg/util/contexthttp.go @@ -0,0 +1,38 @@ +package util + +import ( + "net/http" + "net/url" + + "github.com/go-logr/logr" + + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" +) + +func RequestWithLoggerWithName(r *http.Request, name string) *http.Request { + logger := LoggerFromContext(r.Context()) + logger = logger.WithName(name) + + return RequestWithLogger(r, logger) +} + +func RequestWithLogger(r *http.Request, logger logr.Logger) *http.Request { + ctx := r.Context() + ctx = ContextWithLogger(ctx, logger) + + return r.WithContext(ctx) +} + +func RequestWithHTTPSO(r *http.Request, httpso *httpv1alpha1.HTTPScaledObject) *http.Request { + ctx := r.Context() + ctx = ContextWithHTTPSO(ctx, httpso) + + return r.WithContext(ctx) +} + +func RequestWithStream(r *http.Request, stream *url.URL) *http.Request { + ctx := r.Context() + ctx = ContextWithStream(ctx, stream) + + return r.WithContext(ctx) +} diff --git a/pkg/util/functional.go b/pkg/util/functional.go new file mode 100644 index 000000000..2fe55a3a4 --- /dev/null +++ b/pkg/util/functional.go @@ -0,0 +1,24 @@ +package util + +import ( + "context" +) + +//revive:disable:context-as-argument +func ApplyContext(f func(ctx context.Context) error, ctx context.Context) func() error { + //revive:enable:context-as-argument + return func() error { + return f(ctx) + } +} + +func DeapplyError(f func(), err error) func() error { + return func() error { + f() + return err + } +} + +func IgnoringError(f func() error) { + _ = f() +} diff --git a/pkg/util/healthcheck.go b/pkg/util/healthcheck.go new file mode 100644 index 000000000..e6ea2db59 --- /dev/null +++ b/pkg/util/healthcheck.go @@ -0,0 +1,17 @@ +package util + +import ( + "context" +) + +type HealthChecker interface { + HealthCheck(ctx context.Context) error +} + +type HealthCheckerFunc func(ctx context.Context) error + +var _ HealthChecker = (*HealthCheckerFunc)(nil) + +func (f HealthCheckerFunc) HealthCheck(ctx context.Context) error { + return f(ctx) +} diff --git a/pkg/util/reflect.go b/pkg/util/reflect.go new file mode 100644 index 000000000..8c40e1c15 --- /dev/null +++ b/pkg/util/reflect.go @@ -0,0 +1,25 @@ +package util + +import ( + "reflect" +) + +func IsNil(i interface{}) bool { + if i == nil { + return true + } + + switch v := reflect.ValueOf(i); v.Kind() { + case + reflect.Chan, + reflect.Func, + reflect.Interface, + reflect.Map, + reflect.Pointer, + reflect.Slice, + reflect.UnsafePointer: + return v.IsNil() + } + + return false +} diff --git a/pkg/util/signaler.go b/pkg/util/signaler.go new file mode 100644 index 000000000..9302d2ac8 --- /dev/null +++ b/pkg/util/signaler.go @@ -0,0 +1,34 @@ +package util + +import ( + "context" +) + +type Signaler interface { + Signal() + Wait(ctx context.Context) error +} + +type signaler chan struct{} + +func NewSignaler() Signaler { + return make(signaler, 1) +} + +var _ Signaler = (*signaler)(nil) + +func (s signaler) Signal() { + select { + case s <- struct{}{}: + default: + } +} + +func (s signaler) Wait(ctx context.Context) error { + select { + case <-s: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/pkg/util/signaler_test.go b/pkg/util/signaler_test.go new file mode 100644 index 000000000..a4eea167a --- /dev/null +++ b/pkg/util/signaler_test.go @@ -0,0 +1,91 @@ +package util + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Signaler", func() { + Context("New", func() { + It("returns a signaler of capacity 1", func() { + i := NewSignaler() + + s, ok := i.(signaler) + Expect(ok).To(BeTrue()) + + c := cap(s) + Expect(c).To(Equal(1)) + }) + }) + + Context("Signal", func() { + It("produces on channel", func() { + s := make(signaler, 1) + + err := WithTimeout(time.Second, DeapplyError(s.Signal, nil)) + Expect(err).NotTo(HaveOccurred()) + + select { + case <-s: + default: + Fail("channel should not be empty") + } + }) + + It("does not block when channel is full", func() { + s := make(signaler, 1) + s <- struct{}{} + + err := WithTimeout(time.Second, DeapplyError(s.Signal, nil)) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("Wait", func() { + It("returns nil when channel is not empty", func() { + ctx := context.TODO() + + s := make(signaler, 1) + s <- struct{}{} + + err := WithTimeout(time.Second, ApplyContext(s.Wait, ctx)) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns err when context is done", func() { + ctx, cancel := context.WithCancel(context.TODO()) + cancel() + + s := make(signaler, 1) + + err := WithTimeout(time.Second, ApplyContext(s.Wait, ctx)) + Expect(err).To(MatchError(context.Canceled)) + }) + }) + + Context("E2E", func() { + It("succeeds", func() { + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + s := NewSignaler() + + err0 := WithTimeout(time.Second, DeapplyError(s.Signal, nil)) + Expect(err0).NotTo(HaveOccurred()) + + err1 := WithTimeout(time.Second, DeapplyError(s.Signal, nil)) + Expect(err1).NotTo(HaveOccurred()) + + err2 := WithTimeout(time.Second, ApplyContext(s.Wait, ctx)) + Expect(err2).NotTo(HaveOccurred()) + + cancel() + + err3 := WithTimeout(time.Second, ApplyContext(s.Wait, ctx)) + Expect(err3).To(MatchError(context.Canceled)) + }) + }) +}) diff --git a/pkg/util/stopwatch.go b/pkg/util/stopwatch.go new file mode 100644 index 000000000..919e79ffc --- /dev/null +++ b/pkg/util/stopwatch.go @@ -0,0 +1,30 @@ +package util + +import ( + "time" +) + +type Stopwatch struct { + startTime time.Time + stopTime time.Time +} + +func (sw *Stopwatch) Start() { + sw.startTime = time.Now() +} + +func (sw *Stopwatch) Stop() { + sw.stopTime = time.Now() +} + +func (sw *Stopwatch) StartTime() time.Time { + return sw.startTime +} + +func (sw *Stopwatch) StopTime() time.Time { + return sw.stopTime +} + +func (sw *Stopwatch) ElapsedTime() time.Duration { + return sw.stopTime.Sub(sw.startTime) +} diff --git a/pkg/util/stopwatch_test.go b/pkg/util/stopwatch_test.go new file mode 100644 index 000000000..f5aa751be --- /dev/null +++ b/pkg/util/stopwatch_test.go @@ -0,0 +1,76 @@ +package util + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Stopwatch", func() { + Context("Start", func() { + It("records current time on startTime", func() { + var sw Stopwatch + + sw.Start() + Expect(sw.startTime).To(BeTemporally("~", time.Now(), time.Millisecond)) + }) + }) + + Context("Stop", func() { + It("records current time on stopTime", func() { + var sw Stopwatch + + sw.Stop() + Expect(sw.stopTime).To(BeTemporally("~", time.Now(), time.Millisecond)) + }) + }) + + Context("StartTime", func() { + It("returns the expected value", func() { + var ( + st = time.Now().Add(-time.Minute) + ) + + sw := Stopwatch{ + startTime: st, + } + + ret := sw.StartTime() + Expect(ret).To(Equal(st)) + }) + }) + + Context("StopTime", func() { + It("returns the expected value", func() { + var ( + st = time.Now().Add(+time.Minute) + ) + + sw := Stopwatch{ + stopTime: st, + } + + ret := sw.StopTime() + Expect(ret).To(Equal(st)) + }) + }) + + Context("ElapsedTime", func() { + It("returns the difference between startTime and stopTime", func() { + var ( + at = time.Now().Add(-time.Minute) + ot = time.Now().Add(+time.Minute) + du = ot.Sub(at) + ) + + sw := &Stopwatch{ + startTime: at, + stopTime: ot, + } + + ret := sw.ElapsedTime() + Expect(ret).To(Equal(du)) + }) + }) +}) diff --git a/pkg/util/suite_test.go b/pkg/util/suite_test.go new file mode 100644 index 000000000..b00d5d70d --- /dev/null +++ b/pkg/util/suite_test.go @@ -0,0 +1,14 @@ +package util + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestUtil(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Util Suite") +} diff --git a/proto/scaler.pb.go b/proto/scaler.pb.go deleted file mode 100644 index 8021593c7..000000000 --- a/proto/scaler.pb.go +++ /dev/null @@ -1,622 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.28.1 -// protoc v4.22.0 -// source: scaler.proto - -package externalscaler - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type ScaledObjectRef struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` - ScalerMetadata map[string]string `protobuf:"bytes,3,rep,name=scalerMetadata,proto3" json:"scalerMetadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` -} - -func (x *ScaledObjectRef) Reset() { - *x = ScaledObjectRef{} - if protoimpl.UnsafeEnabled { - mi := &file_scaler_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *ScaledObjectRef) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ScaledObjectRef) ProtoMessage() {} - -func (x *ScaledObjectRef) ProtoReflect() protoreflect.Message { - mi := &file_scaler_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ScaledObjectRef.ProtoReflect.Descriptor instead. -func (*ScaledObjectRef) Descriptor() ([]byte, []int) { - return file_scaler_proto_rawDescGZIP(), []int{0} -} - -func (x *ScaledObjectRef) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *ScaledObjectRef) GetNamespace() string { - if x != nil { - return x.Namespace - } - return "" -} - -func (x *ScaledObjectRef) GetScalerMetadata() map[string]string { - if x != nil { - return x.ScalerMetadata - } - return nil -} - -type IsActiveResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Result bool `protobuf:"varint,1,opt,name=result,proto3" json:"result,omitempty"` -} - -func (x *IsActiveResponse) Reset() { - *x = IsActiveResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_scaler_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *IsActiveResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*IsActiveResponse) ProtoMessage() {} - -func (x *IsActiveResponse) ProtoReflect() protoreflect.Message { - mi := &file_scaler_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use IsActiveResponse.ProtoReflect.Descriptor instead. -func (*IsActiveResponse) Descriptor() ([]byte, []int) { - return file_scaler_proto_rawDescGZIP(), []int{1} -} - -func (x *IsActiveResponse) GetResult() bool { - if x != nil { - return x.Result - } - return false -} - -type GetMetricSpecResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - MetricSpecs []*MetricSpec `protobuf:"bytes,1,rep,name=metricSpecs,proto3" json:"metricSpecs,omitempty"` -} - -func (x *GetMetricSpecResponse) Reset() { - *x = GetMetricSpecResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_scaler_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GetMetricSpecResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetMetricSpecResponse) ProtoMessage() {} - -func (x *GetMetricSpecResponse) ProtoReflect() protoreflect.Message { - mi := &file_scaler_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetMetricSpecResponse.ProtoReflect.Descriptor instead. -func (*GetMetricSpecResponse) Descriptor() ([]byte, []int) { - return file_scaler_proto_rawDescGZIP(), []int{2} -} - -func (x *GetMetricSpecResponse) GetMetricSpecs() []*MetricSpec { - if x != nil { - return x.MetricSpecs - } - return nil -} - -type MetricSpec struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - MetricName string `protobuf:"bytes,1,opt,name=metricName,proto3" json:"metricName,omitempty"` - TargetSize int64 `protobuf:"varint,2,opt,name=targetSize,proto3" json:"targetSize,omitempty"` -} - -func (x *MetricSpec) Reset() { - *x = MetricSpec{} - if protoimpl.UnsafeEnabled { - mi := &file_scaler_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *MetricSpec) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MetricSpec) ProtoMessage() {} - -func (x *MetricSpec) ProtoReflect() protoreflect.Message { - mi := &file_scaler_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use MetricSpec.ProtoReflect.Descriptor instead. -func (*MetricSpec) Descriptor() ([]byte, []int) { - return file_scaler_proto_rawDescGZIP(), []int{3} -} - -func (x *MetricSpec) GetMetricName() string { - if x != nil { - return x.MetricName - } - return "" -} - -func (x *MetricSpec) GetTargetSize() int64 { - if x != nil { - return x.TargetSize - } - return 0 -} - -type GetMetricsRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ScaledObjectRef *ScaledObjectRef `protobuf:"bytes,1,opt,name=scaledObjectRef,proto3" json:"scaledObjectRef,omitempty"` - MetricName string `protobuf:"bytes,2,opt,name=metricName,proto3" json:"metricName,omitempty"` -} - -func (x *GetMetricsRequest) Reset() { - *x = GetMetricsRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_scaler_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GetMetricsRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetMetricsRequest) ProtoMessage() {} - -func (x *GetMetricsRequest) ProtoReflect() protoreflect.Message { - mi := &file_scaler_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetMetricsRequest.ProtoReflect.Descriptor instead. -func (*GetMetricsRequest) Descriptor() ([]byte, []int) { - return file_scaler_proto_rawDescGZIP(), []int{4} -} - -func (x *GetMetricsRequest) GetScaledObjectRef() *ScaledObjectRef { - if x != nil { - return x.ScaledObjectRef - } - return nil -} - -func (x *GetMetricsRequest) GetMetricName() string { - if x != nil { - return x.MetricName - } - return "" -} - -type GetMetricsResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - MetricValues []*MetricValue `protobuf:"bytes,1,rep,name=metricValues,proto3" json:"metricValues,omitempty"` -} - -func (x *GetMetricsResponse) Reset() { - *x = GetMetricsResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_scaler_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GetMetricsResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetMetricsResponse) ProtoMessage() {} - -func (x *GetMetricsResponse) ProtoReflect() protoreflect.Message { - mi := &file_scaler_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetMetricsResponse.ProtoReflect.Descriptor instead. -func (*GetMetricsResponse) Descriptor() ([]byte, []int) { - return file_scaler_proto_rawDescGZIP(), []int{5} -} - -func (x *GetMetricsResponse) GetMetricValues() []*MetricValue { - if x != nil { - return x.MetricValues - } - return nil -} - -type MetricValue struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - MetricName string `protobuf:"bytes,1,opt,name=metricName,proto3" json:"metricName,omitempty"` - MetricValue int64 `protobuf:"varint,2,opt,name=metricValue,proto3" json:"metricValue,omitempty"` -} - -func (x *MetricValue) Reset() { - *x = MetricValue{} - if protoimpl.UnsafeEnabled { - mi := &file_scaler_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *MetricValue) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*MetricValue) ProtoMessage() {} - -func (x *MetricValue) ProtoReflect() protoreflect.Message { - mi := &file_scaler_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use MetricValue.ProtoReflect.Descriptor instead. -func (*MetricValue) Descriptor() ([]byte, []int) { - return file_scaler_proto_rawDescGZIP(), []int{6} -} - -func (x *MetricValue) GetMetricName() string { - if x != nil { - return x.MetricName - } - return "" -} - -func (x *MetricValue) GetMetricValue() int64 { - if x != nil { - return x.MetricValue - } - return 0 -} - -var File_scaler_proto protoreflect.FileDescriptor - -var file_scaler_proto_rawDesc = []byte{ - 0x0a, 0x0c, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, - 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x22, 0xe3, - 0x01, 0x0a, 0x0f, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, - 0x65, 0x66, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x0e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x53, 0x63, - 0x61, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x66, 0x2e, 0x53, 0x63, - 0x61, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x0e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x1a, 0x41, 0x0a, 0x13, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x22, 0x2a, 0x0a, 0x10, 0x49, 0x73, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x22, 0x55, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x53, 0x70, 0x65, - 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0b, 0x6d, 0x65, 0x74, - 0x72, 0x69, 0x63, 0x53, 0x70, 0x65, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x2e, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x53, 0x70, 0x65, 0x63, 0x52, 0x0b, 0x6d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x53, 0x70, 0x65, 0x63, 0x73, 0x22, 0x4c, 0x0a, 0x0a, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, - 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x7e, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x49, 0x0a, 0x0f, 0x73, 0x63, - 0x61, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x66, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, - 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x52, 0x65, 0x66, 0x52, 0x0f, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x52, 0x65, 0x66, 0x12, 0x1e, 0x0a, 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x55, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x0c, 0x6d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1b, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, 0x61, 0x6c, - 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, - 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x4f, 0x0a, 0x0b, - 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x6d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, - 0x52, 0x0b, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x32, 0xec, 0x02, - 0x0a, 0x0e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x72, - 0x12, 0x4f, 0x0a, 0x08, 0x49, 0x73, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x1f, 0x2e, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x53, 0x63, - 0x61, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x66, 0x1a, 0x20, 0x2e, - 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x49, - 0x73, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x57, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x49, 0x73, 0x41, 0x63, 0x74, - 0x69, 0x76, 0x65, 0x12, 0x1f, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, - 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x53, 0x63, 0x61, 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x52, 0x65, 0x66, 0x1a, 0x20, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, - 0x63, 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x49, 0x73, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x59, 0x0a, 0x0d, 0x47, 0x65, - 0x74, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x53, 0x70, 0x65, 0x63, 0x12, 0x1f, 0x2e, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x53, 0x63, 0x61, - 0x6c, 0x65, 0x64, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x66, 0x1a, 0x25, 0x2e, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x47, 0x65, - 0x74, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x53, 0x70, 0x65, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x55, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x73, 0x12, 0x21, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, - 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x12, 0x5a, 0x10, - 0x2e, 0x3b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x72, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_scaler_proto_rawDescOnce sync.Once - file_scaler_proto_rawDescData = file_scaler_proto_rawDesc -) - -func file_scaler_proto_rawDescGZIP() []byte { - file_scaler_proto_rawDescOnce.Do(func() { - file_scaler_proto_rawDescData = protoimpl.X.CompressGZIP(file_scaler_proto_rawDescData) - }) - return file_scaler_proto_rawDescData -} - -var file_scaler_proto_msgTypes = make([]protoimpl.MessageInfo, 8) -var file_scaler_proto_goTypes = []interface{}{ - (*ScaledObjectRef)(nil), // 0: externalscaler.ScaledObjectRef - (*IsActiveResponse)(nil), // 1: externalscaler.IsActiveResponse - (*GetMetricSpecResponse)(nil), // 2: externalscaler.GetMetricSpecResponse - (*MetricSpec)(nil), // 3: externalscaler.MetricSpec - (*GetMetricsRequest)(nil), // 4: externalscaler.GetMetricsRequest - (*GetMetricsResponse)(nil), // 5: externalscaler.GetMetricsResponse - (*MetricValue)(nil), // 6: externalscaler.MetricValue - nil, // 7: externalscaler.ScaledObjectRef.ScalerMetadataEntry -} -var file_scaler_proto_depIdxs = []int32{ - 7, // 0: externalscaler.ScaledObjectRef.scalerMetadata:type_name -> externalscaler.ScaledObjectRef.ScalerMetadataEntry - 3, // 1: externalscaler.GetMetricSpecResponse.metricSpecs:type_name -> externalscaler.MetricSpec - 0, // 2: externalscaler.GetMetricsRequest.scaledObjectRef:type_name -> externalscaler.ScaledObjectRef - 6, // 3: externalscaler.GetMetricsResponse.metricValues:type_name -> externalscaler.MetricValue - 0, // 4: externalscaler.ExternalScaler.IsActive:input_type -> externalscaler.ScaledObjectRef - 0, // 5: externalscaler.ExternalScaler.StreamIsActive:input_type -> externalscaler.ScaledObjectRef - 0, // 6: externalscaler.ExternalScaler.GetMetricSpec:input_type -> externalscaler.ScaledObjectRef - 4, // 7: externalscaler.ExternalScaler.GetMetrics:input_type -> externalscaler.GetMetricsRequest - 1, // 8: externalscaler.ExternalScaler.IsActive:output_type -> externalscaler.IsActiveResponse - 1, // 9: externalscaler.ExternalScaler.StreamIsActive:output_type -> externalscaler.IsActiveResponse - 2, // 10: externalscaler.ExternalScaler.GetMetricSpec:output_type -> externalscaler.GetMetricSpecResponse - 5, // 11: externalscaler.ExternalScaler.GetMetrics:output_type -> externalscaler.GetMetricsResponse - 8, // [8:12] is the sub-list for method output_type - 4, // [4:8] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name -} - -func init() { file_scaler_proto_init() } -func file_scaler_proto_init() { - if File_scaler_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_scaler_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ScaledObjectRef); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_scaler_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IsActiveResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_scaler_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetMetricSpecResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_scaler_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MetricSpec); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_scaler_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetMetricsRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_scaler_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetMetricsResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_scaler_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*MetricValue); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_scaler_proto_rawDesc, - NumEnums: 0, - NumMessages: 8, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_scaler_proto_goTypes, - DependencyIndexes: file_scaler_proto_depIdxs, - MessageInfos: file_scaler_proto_msgTypes, - }.Build() - File_scaler_proto = out.File - file_scaler_proto_rawDesc = nil - file_scaler_proto_goTypes = nil - file_scaler_proto_depIdxs = nil -} diff --git a/proto/scaler.proto b/proto/scaler.proto deleted file mode 100644 index 1df449545..000000000 --- a/proto/scaler.proto +++ /dev/null @@ -1,44 +0,0 @@ -syntax = "proto3"; - -package externalscaler; -option go_package = ".;externalscaler"; - -service ExternalScaler { - rpc IsActive(ScaledObjectRef) returns (IsActiveResponse) {} - rpc StreamIsActive(ScaledObjectRef) returns (stream IsActiveResponse) {} - rpc GetMetricSpec(ScaledObjectRef) returns (GetMetricSpecResponse) {} - rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse) {} -} - -message ScaledObjectRef { - string name = 1; - string namespace = 2; - map scalerMetadata = 3; -} - -message IsActiveResponse { - bool result = 1; -} - -message GetMetricSpecResponse { - repeated MetricSpec metricSpecs = 1; -} - -message MetricSpec { - string metricName = 1; - int64 targetSize = 2; -} - -message GetMetricsRequest { - ScaledObjectRef scaledObjectRef = 1; - string metricName = 2; -} - -message GetMetricsResponse { - repeated MetricValue metricValues = 1; -} - -message MetricValue { - string metricName = 1; - int64 metricValue = 2; -} diff --git a/proto/scaler_grpc.pb.go b/proto/scaler_grpc.pb.go deleted file mode 100644 index 60347e014..000000000 --- a/proto/scaler_grpc.pb.go +++ /dev/null @@ -1,241 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. -// versions: -// - protoc-gen-go-grpc v1.2.0 -// - protoc v4.22.0 -// source: scaler.proto - -package externalscaler - -import ( - context "context" - grpc "google.golang.org/grpc" - codes "google.golang.org/grpc/codes" - status "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 - -// ExternalScalerClient is the client API for ExternalScaler service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type ExternalScalerClient interface { - IsActive(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (*IsActiveResponse, error) - StreamIsActive(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (ExternalScaler_StreamIsActiveClient, error) - GetMetricSpec(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (*GetMetricSpecResponse, error) - GetMetrics(ctx context.Context, in *GetMetricsRequest, opts ...grpc.CallOption) (*GetMetricsResponse, error) -} - -type externalScalerClient struct { - cc grpc.ClientConnInterface -} - -func NewExternalScalerClient(cc grpc.ClientConnInterface) ExternalScalerClient { - return &externalScalerClient{cc} -} - -func (c *externalScalerClient) IsActive(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (*IsActiveResponse, error) { - out := new(IsActiveResponse) - err := c.cc.Invoke(ctx, "/externalscaler.ExternalScaler/IsActive", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *externalScalerClient) StreamIsActive(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (ExternalScaler_StreamIsActiveClient, error) { - stream, err := c.cc.NewStream(ctx, &ExternalScaler_ServiceDesc.Streams[0], "/externalscaler.ExternalScaler/StreamIsActive", opts...) - if err != nil { - return nil, err - } - x := &externalScalerStreamIsActiveClient{stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -type ExternalScaler_StreamIsActiveClient interface { - Recv() (*IsActiveResponse, error) - grpc.ClientStream -} - -type externalScalerStreamIsActiveClient struct { - grpc.ClientStream -} - -func (x *externalScalerStreamIsActiveClient) Recv() (*IsActiveResponse, error) { - m := new(IsActiveResponse) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *externalScalerClient) GetMetricSpec(ctx context.Context, in *ScaledObjectRef, opts ...grpc.CallOption) (*GetMetricSpecResponse, error) { - out := new(GetMetricSpecResponse) - err := c.cc.Invoke(ctx, "/externalscaler.ExternalScaler/GetMetricSpec", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *externalScalerClient) GetMetrics(ctx context.Context, in *GetMetricsRequest, opts ...grpc.CallOption) (*GetMetricsResponse, error) { - out := new(GetMetricsResponse) - err := c.cc.Invoke(ctx, "/externalscaler.ExternalScaler/GetMetrics", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// ExternalScalerServer is the server API for ExternalScaler service. -// All implementations must embed UnimplementedExternalScalerServer -// for forward compatibility -type ExternalScalerServer interface { - IsActive(context.Context, *ScaledObjectRef) (*IsActiveResponse, error) - StreamIsActive(*ScaledObjectRef, ExternalScaler_StreamIsActiveServer) error - GetMetricSpec(context.Context, *ScaledObjectRef) (*GetMetricSpecResponse, error) - GetMetrics(context.Context, *GetMetricsRequest) (*GetMetricsResponse, error) - mustEmbedUnimplementedExternalScalerServer() -} - -// UnimplementedExternalScalerServer must be embedded to have forward compatible implementations. -type UnimplementedExternalScalerServer struct { -} - -func (UnimplementedExternalScalerServer) IsActive(context.Context, *ScaledObjectRef) (*IsActiveResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method IsActive not implemented") -} -func (UnimplementedExternalScalerServer) StreamIsActive(*ScaledObjectRef, ExternalScaler_StreamIsActiveServer) error { - return status.Errorf(codes.Unimplemented, "method StreamIsActive not implemented") -} -func (UnimplementedExternalScalerServer) GetMetricSpec(context.Context, *ScaledObjectRef) (*GetMetricSpecResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetMetricSpec not implemented") -} -func (UnimplementedExternalScalerServer) GetMetrics(context.Context, *GetMetricsRequest) (*GetMetricsResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetMetrics not implemented") -} -func (UnimplementedExternalScalerServer) mustEmbedUnimplementedExternalScalerServer() {} - -// UnsafeExternalScalerServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to ExternalScalerServer will -// result in compilation errors. -type UnsafeExternalScalerServer interface { - mustEmbedUnimplementedExternalScalerServer() -} - -func RegisterExternalScalerServer(s grpc.ServiceRegistrar, srv ExternalScalerServer) { - s.RegisterService(&ExternalScaler_ServiceDesc, srv) -} - -func _ExternalScaler_IsActive_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ScaledObjectRef) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ExternalScalerServer).IsActive(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/externalscaler.ExternalScaler/IsActive", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ExternalScalerServer).IsActive(ctx, req.(*ScaledObjectRef)) - } - return interceptor(ctx, in, info, handler) -} - -func _ExternalScaler_StreamIsActive_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(ScaledObjectRef) - if err := stream.RecvMsg(m); err != nil { - return err - } - return srv.(ExternalScalerServer).StreamIsActive(m, &externalScalerStreamIsActiveServer{stream}) -} - -type ExternalScaler_StreamIsActiveServer interface { - Send(*IsActiveResponse) error - grpc.ServerStream -} - -type externalScalerStreamIsActiveServer struct { - grpc.ServerStream -} - -func (x *externalScalerStreamIsActiveServer) Send(m *IsActiveResponse) error { - return x.ServerStream.SendMsg(m) -} - -func _ExternalScaler_GetMetricSpec_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ScaledObjectRef) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ExternalScalerServer).GetMetricSpec(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/externalscaler.ExternalScaler/GetMetricSpec", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ExternalScalerServer).GetMetricSpec(ctx, req.(*ScaledObjectRef)) - } - return interceptor(ctx, in, info, handler) -} - -func _ExternalScaler_GetMetrics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetMetricsRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ExternalScalerServer).GetMetrics(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/externalscaler.ExternalScaler/GetMetrics", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ExternalScalerServer).GetMetrics(ctx, req.(*GetMetricsRequest)) - } - return interceptor(ctx, in, info, handler) -} - -// ExternalScaler_ServiceDesc is the grpc.ServiceDesc for ExternalScaler service. -// It's only intended for direct use with grpc.RegisterService, -// and not to be introspected or modified (even as a copy) -var ExternalScaler_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "externalscaler.ExternalScaler", - HandlerType: (*ExternalScalerServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "IsActive", - Handler: _ExternalScaler_IsActive_Handler, - }, - { - MethodName: "GetMetricSpec", - Handler: _ExternalScaler_GetMetricSpec_Handler, - }, - { - MethodName: "GetMetrics", - Handler: _ExternalScaler_GetMetrics_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "StreamIsActive", - Handler: _ExternalScaler_StreamIsActive_Handler, - ServerStreams: true, - }, - }, - Metadata: "scaler.proto", -} diff --git a/scaler/Dockerfile b/scaler/Dockerfile index d9575e8b0..5416a5975 100644 --- a/scaler/Dockerfile +++ b/scaler/Dockerfile @@ -1,28 +1,14 @@ -# Build the adapter binary -FROM --platform=$BUILDPLATFORM ghcr.io/kedacore/build-tools:1.19.5 as builder - -ARG VERSION=main -ARG GIT_COMMIT=HEAD - +FROM --platform=${BUILDPLATFORM} ghcr.io/kedacore/build-tools:1.20.4 as builder WORKDIR /workspace - -COPY go.mod go.mod -COPY go.sum go.sum +COPY go.* . RUN go mod download - COPY . . - -# Build -# https://www.docker.com/blog/faster-multi-platform-builds-dockerfile-cross-compilation-guide/ +ARG VERSION=main +ARG GIT_COMMIT=HEAD ARG TARGETOS ARG TARGETARCH -RUN VERSION=${VERSION} GIT_COMMIT=${GIT_COMMIT} TARGET_OS=$TARGETOS ARCH=$TARGETARCH make build-scaler +RUN VERSION="${VERSION}" GIT_COMMIT="${GIT_COMMIT}" TARGET_OS="${TARGETOS}" ARCH="${TARGETARCH}" make build-scaler FROM gcr.io/distroless/static:nonroot -WORKDIR / -COPY --from=builder /workspace/bin/scaler . -# 65532 is numeric for nonroot -USER 65532:65532 -EXPOSE 8080 - -ENTRYPOINT ["/scaler"] +COPY --from=builder /workspace/bin/scaler /sbin/init +ENTRYPOINT ["/sbin/init"] diff --git a/scaler/handlers.go b/scaler/handlers.go index b18d86fb7..d24aaf378 100644 --- a/scaler/handlers.go +++ b/scaler/handlers.go @@ -5,31 +5,27 @@ package main import ( - context "context" - "fmt" + "context" "math/rand" "time" "github.com/go-logr/logr" + "github.com/kedacore/keda/v2/pkg/scalers/externalscaler" "google.golang.org/protobuf/types/known/emptypb" + "k8s.io/utils/pointer" - "github.com/kedacore/http-add-on/pkg/routing" - externalscaler "github.com/kedacore/http-add-on/proto" + informershttpv1alpha1 "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1" + "github.com/kedacore/http-add-on/pkg/k8s" ) func init() { rand.Seed(time.Now().UnixNano()) } -const ( - interceptor = "interceptor" - httpRequests = "http-requests" -) - type impl struct { lggr logr.Logger pinger *queuePinger - routingTable routing.TableReader + httpsoInformer informershttpv1alpha1.HTTPScaledObjectInformer targetMetric int64 targetMetricInterceptor int64 externalscaler.UnimplementedExternalScalerServer @@ -38,14 +34,14 @@ type impl struct { func newImpl( lggr logr.Logger, pinger *queuePinger, - routingTable routing.TableReader, + httpsoInformer informershttpv1alpha1.HTTPScaledObjectInformer, defaultTargetMetric int64, defaultTargetMetricInterceptor int64, ) *impl { return &impl{ lggr: lggr, pinger: pinger, - routingTable: routingTable, + httpsoInformer: httpsoInformer, targetMetric: defaultTargetMetric, targetMetricInterceptor: defaultTargetMetricInterceptor, } @@ -56,45 +52,19 @@ func (e *impl) Ping(context.Context, *emptypb.Empty) (*emptypb.Empty, error) { } func (e *impl) IsActive( - ctx context.Context, + _ context.Context, scaledObject *externalscaler.ScaledObjectRef, ) (*externalscaler.IsActiveResponse, error) { - lggr := e.lggr.WithName("IsActive") + namespacedName := k8s.NamespacedNameFromScaledObjectRef(scaledObject) - hosts, err := getHostsFromScaledObjectRef(lggr, scaledObject) - if err != nil { - return nil, err - } + key := namespacedName.String() + count := e.pinger.counts()[key] - totalHostCount := 0 - for _, host := range hosts { - if host == interceptor { - return &externalscaler.IsActiveResponse{ - Result: true, - }, nil - } - - hostCount, ok := getHostCount( - host, - e.pinger.counts(), - e.routingTable, - ) - if !ok { - err := fmt.Errorf("host '%s' not found in counts", host) - allCounts := e.pinger.mergeCountsWithRoutingTable( - e.routingTable, - ) - lggr.Error(err, "Given host was not found in queue count map", "host", host, "allCounts", allCounts) - return nil, err - } - - totalHostCount += hostCount - } - - active := totalHostCount > 0 - return &externalscaler.IsActiveResponse{ + active := count > 0 + res := &externalscaler.IsActiveResponse{ Result: active, - }, nil + } + return res, nil } func (e *impl) StreamIsActive( @@ -139,82 +109,46 @@ func (e *impl) GetMetricSpec( ) (*externalscaler.GetMetricSpecResponse, error) { lggr := e.lggr.WithName("GetMetricSpec") - hosts, err := getHostsFromScaledObjectRef(lggr, sor) + namespacedName := k8s.NamespacedNameFromScaledObjectRef(sor) + metricName := MetricName(namespacedName) + + httpso, err := e.httpsoInformer.Lister().HTTPScaledObjects(sor.Namespace).Get(sor.Name) if err != nil { + lggr.Error(err, "unable to get HTTPScaledObject", "name", sor.Name, "namespace", sor.Namespace) return nil, err } - - var targetPendingRequests int64 - var host = hosts[0] // We are only interested in the first host to get the targetPendingRequests - - if host == interceptor { - targetPendingRequests = e.targetMetricInterceptor - } else { - target, err := e.routingTable.Lookup(host) - if err != nil { - lggr.Error( - err, - "error getting target for host", - "host", - host, - ) - return nil, err - } - host = httpRequests - targetPendingRequests = int64(target.TargetPendingRequests) - } - - metricSpecs := []*externalscaler.MetricSpec{ - { - MetricName: host, - TargetSize: targetPendingRequests, + targetPendingRequests := int64(pointer.Int32Deref(httpso.Spec.TargetPendingRequests, 100)) + + res := &externalscaler.GetMetricSpecResponse{ + MetricSpecs: []*externalscaler.MetricSpec{ + { + MetricName: metricName, + TargetSize: targetPendingRequests, + }, }, } - return &externalscaler.GetMetricSpecResponse{ - MetricSpecs: metricSpecs, - }, nil + return res, nil } func (e *impl) GetMetrics( _ context.Context, metricRequest *externalscaler.GetMetricsRequest, ) (*externalscaler.GetMetricsResponse, error) { - lggr := e.lggr.WithName("GetMetrics") + sor := metricRequest.ScaledObjectRef - hosts, err := getHostsFromScaledObjectRef(lggr, metricRequest.ScaledObjectRef) - if err != nil { - return nil, err - } + namespacedName := k8s.NamespacedNameFromScaledObjectRef(sor) + metricName := MetricName(namespacedName) - var totalCount int64 - var metricName = httpRequests - for _, host := range hosts { - hostCount, ok := getHostCount( - host, - e.pinger.counts(), - e.routingTable, - ) - if !ok { - if host != interceptor { - err := fmt.Errorf("host '%s' not found in counts", host) - allCounts := e.pinger.mergeCountsWithRoutingTable(e.routingTable) - lggr.Error(err, "allCounts", allCounts) - return nil, err - } - hostCount = e.pinger.aggregate() - metricName = interceptor - } - totalCount += int64(hostCount) - } + key := namespacedName.String() + count := e.pinger.counts()[key] - metricValues := []*externalscaler.MetricValue{ - { - MetricName: metricName, - MetricValue: totalCount, + res := &externalscaler.GetMetricsResponse{ + MetricValues: []*externalscaler.MetricValue{ + { + MetricName: metricName, + MetricValue: int64(count), + }, }, } - - return &externalscaler.GetMetricsResponse{ - MetricValues: metricValues, - }, nil + return res, nil } diff --git a/scaler/handlers_test.go b/scaler/handlers_test.go index bc7d3620e..251fe790e 100644 --- a/scaler/handlers_test.go +++ b/scaler/handlers_test.go @@ -1,120 +1,121 @@ package main import ( - context "context" + "context" "fmt" "net" "testing" "time" "github.com/go-logr/logr" + "github.com/golang/mock/gomock" + "github.com/kedacore/keda/v2/pkg/scalers/externalscaler" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/test/bufconn" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" + httpv1alpha1 "github.com/kedacore/http-add-on/operator/apis/http/v1alpha1" + informersexternalversionshttpv1alpha1mock "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1/mock" + listershttpv1alpha1mock "github.com/kedacore/http-add-on/operator/generated/listers/http/v1alpha1/mock" "github.com/kedacore/http-add-on/pkg/queue" - "github.com/kedacore/http-add-on/pkg/routing" - externalscaler "github.com/kedacore/http-add-on/proto" ) -func standardTarget() routing.Target { - return routing.NewTarget( - "testns", - "testsrv", - 8080, - "testdepl", - 123, - ) -} - func TestStreamIsActive(t *testing.T) { type testCase struct { name string - hosts string expected bool expectedErr bool - setup func(*routing.Table, *queuePinger) + setup func(t *testing.T, qp *queuePinger) } - r := require.New(t) testCases := []testCase{ { name: "Simple host inactive", - hosts: t.Name(), expected: false, expectedErr: false, - setup: func(table *routing.Table, q *queuePinger) { - r.NoError(table.AddTarget(t.Name(), standardTarget())) - q.pingMut.Lock() - defer q.pingMut.Unlock() - q.allCounts[t.Name()] = 0 + setup: func(t *testing.T, qp *queuePinger) { + namespacedName := &types.NamespacedName{ + Namespace: "default", + Name: t.Name(), + } + key := namespacedName.String() + + qp.pingMut.Lock() + defer qp.pingMut.Unlock() + + qp.allCounts[key] = 0 }, }, - { - name: "Host is 'interceptor'", - hosts: "interceptor", - expected: true, - expectedErr: false, - setup: func(*routing.Table, *queuePinger) {}, - }, { name: "Simple host active", - hosts: t.Name(), expected: true, expectedErr: false, - setup: func(table *routing.Table, q *queuePinger) { - r.NoError(table.AddTarget(t.Name(), standardTarget())) - q.pingMut.Lock() - defer q.pingMut.Unlock() - q.allCounts[t.Name()] = 1 + setup: func(t *testing.T, qp *queuePinger) { + namespacedName := &types.NamespacedName{ + Namespace: "default", + Name: t.Name(), + } + key := namespacedName.String() + + qp.pingMut.Lock() + defer qp.pingMut.Unlock() + + qp.allCounts[key] = 1 }, }, { name: "Simple multi host active", - hosts: "host1,host2", expected: true, expectedErr: false, - setup: func(table *routing.Table, q *queuePinger) { - r.NoError(table.AddTarget(t.Name(), standardTarget())) - q.pingMut.Lock() - defer q.pingMut.Unlock() - q.allCounts["host1"] = 1 - q.allCounts["host2"] = 1 + setup: func(t *testing.T, qp *queuePinger) { + namespacedName := &types.NamespacedName{ + Namespace: "default", + Name: t.Name(), + } + key := namespacedName.String() + + qp.pingMut.Lock() + defer qp.pingMut.Unlock() + + qp.allCounts[key] = 2 }, }, { name: "No host present, but host in routing table", - hosts: t.Name(), expected: false, expectedErr: false, - setup: func(table *routing.Table, q *queuePinger) { - r.NoError(table.AddTarget(t.Name(), standardTarget())) - }, + setup: func(_ *testing.T, _ *queuePinger) {}, }, { name: "Host doesn't exist", - hosts: t.Name(), expected: false, expectedErr: true, - setup: func(*routing.Table, *queuePinger) {}, + setup: func(_ *testing.T, _ *queuePinger) {}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + r := require.New(t) ctx := context.Background() lggr := logr.Discard() - table := routing.NewTable() + informer, _, _ := newMocks(ctrl) ticker, pinger, err := newFakeQueuePinger(ctx, lggr) r.NoError(err) defer ticker.Stop() - tc.setup(table, pinger) + tc.setup(t, pinger) hdl := newImpl( lggr, pinger, - table, + informer, 123, 200, ) @@ -144,9 +145,8 @@ func TestStreamIsActive(t *testing.T) { client := externalscaler.NewExternalScalerClient(conn) testRef := &externalscaler.ScaledObjectRef{ - ScalerMetadata: map[string]string{ - "hosts": tc.hosts, - }, + Namespace: "default", + Name: t.Name(), } // First will see if we can establish the stream and handle this @@ -162,7 +162,8 @@ func TestStreamIsActive(t *testing.T) { if tc.expectedErr && err != nil { return - } else if err != nil { + } + if err != nil { t.Fatalf("expected no error but got: %v", err) } @@ -176,89 +177,93 @@ func TestStreamIsActive(t *testing.T) { func TestIsActive(t *testing.T) { type testCase struct { name string - hosts string expected bool expectedErr bool - setup func(*routing.Table, *queuePinger) + setup func(t *testing.T, qp *queuePinger) } - r := require.New(t) testCases := []testCase{ { name: "Simple host inactive", - hosts: t.Name(), expected: false, expectedErr: false, - setup: func(table *routing.Table, q *queuePinger) { - r.NoError(table.AddTarget(t.Name(), standardTarget())) - q.pingMut.Lock() - defer q.pingMut.Unlock() - q.allCounts[t.Name()] = 0 + setup: func(t *testing.T, qp *queuePinger) { + namespacedName := &types.NamespacedName{ + Namespace: "default", + Name: t.Name(), + } + key := namespacedName.String() + + qp.pingMut.Lock() + defer qp.pingMut.Unlock() + + qp.allCounts[key] = 0 }, }, - { - name: "Host is 'interceptor'", - hosts: "interceptor", - expected: true, - expectedErr: false, - setup: func(*routing.Table, *queuePinger) {}, - }, { name: "Simple host active", - hosts: t.Name(), expected: true, expectedErr: false, - setup: func(table *routing.Table, q *queuePinger) { - r.NoError(table.AddTarget(t.Name(), standardTarget())) - q.pingMut.Lock() - defer q.pingMut.Unlock() - q.allCounts[t.Name()] = 1 + setup: func(t *testing.T, qp *queuePinger) { + namespacedName := &types.NamespacedName{ + Namespace: "default", + Name: t.Name(), + } + key := namespacedName.String() + + qp.pingMut.Lock() + defer qp.pingMut.Unlock() + + qp.allCounts[key] = 1 }, }, { name: "Simple multi host active", - hosts: "host1,host2", expected: true, expectedErr: false, - setup: func(table *routing.Table, q *queuePinger) { - r.NoError(table.AddTarget(t.Name(), standardTarget())) - q.pingMut.Lock() - defer q.pingMut.Unlock() - q.allCounts["host1"] = 1 - q.allCounts["host2"] = 1 + setup: func(t *testing.T, qp *queuePinger) { + namespacedName := &types.NamespacedName{ + Namespace: "default", + Name: t.Name(), + } + key := namespacedName.String() + + qp.pingMut.Lock() + defer qp.pingMut.Unlock() + + qp.allCounts[key] = 2 }, }, { name: "No host present, but host in routing table", - hosts: t.Name(), expected: false, expectedErr: false, - setup: func(table *routing.Table, q *queuePinger) { - r.NoError(table.AddTarget(t.Name(), standardTarget())) - }, + setup: func(_ *testing.T, _ *queuePinger) {}, }, { name: "Host doesn't exist", - hosts: t.Name(), expected: false, expectedErr: true, - setup: func(*routing.Table, *queuePinger) {}, + setup: func(_ *testing.T, _ *queuePinger) {}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + r := require.New(t) ctx := context.Background() lggr := logr.Discard() - table := routing.NewTable() + informer, _, _ := newMocks(ctrl) ticker, pinger, err := newFakeQueuePinger(ctx, lggr) r.NoError(err) defer ticker.Stop() - tc.setup(table, pinger) + tc.setup(t, pinger) hdl := newImpl( lggr, pinger, - table, + informer, 123, 200, ) @@ -266,15 +271,15 @@ func TestIsActive(t *testing.T) { res, err := hdl.IsActive( ctx, &externalscaler.ScaledObjectRef{ - ScalerMetadata: map[string]string{ - "hosts": tc.hosts, - }, + Namespace: "default", + Name: t.Name(), }, ) if tc.expectedErr && err != nil { return - } else if err != nil { + } + if err != nil { t.Fatalf("expected no error but got: %v", err) } if tc.expected != res.Result { @@ -290,30 +295,36 @@ func TestGetMetricSpecTable(t *testing.T) { name string defaultTargetMetric int64 defaultTargetMetricInterceptor int64 - scalerMetadata map[string]string - newRoutingTableFn func() *routing.Table + newInformer func(*testing.T, *gomock.Controller) *informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer checker func(*testing.T, *externalscaler.GetMetricSpecResponse, error) } - r := require.New(t) cases := []testCase{ { name: "valid host as single host value in scaler metadata", defaultTargetMetric: 0, defaultTargetMetricInterceptor: 123, - scalerMetadata: map[string]string{ - "hosts": "validHost", - "targetPendingRequests": "123", - }, - newRoutingTableFn: func() *routing.Table { - ret := routing.NewTable() - r.NoError(ret.AddTarget("validHost", routing.NewTarget( - ns, - "testsrv", - 8080, - "testdepl", - 123, - ))) - return ret + newInformer: func(t *testing.T, ctrl *gomock.Controller) *informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer { + informer, _, namespaceLister := newMocks(ctrl) + + httpso := &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: t.Name(), + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + ScaleTargetRef: httpv1alpha1.ScaleTargetRef{ + Deployment: "testdepl", + Service: "testsrv", + Port: 8080, + }, + TargetPendingRequests: pointer.Int32(123), + }, + } + namespaceLister.EXPECT(). + Get(httpso.GetName()). + Return(httpso, nil) + + return informer }, checker: func(t *testing.T, res *externalscaler.GetMetricSpecResponse, err error) { t.Helper() @@ -322,7 +333,7 @@ func TestGetMetricSpecTable(t *testing.T) { r.NotNil(res) r.Equal(1, len(res.MetricSpecs)) spec := res.MetricSpecs[0] - r.Equal(httpRequests, spec.MetricName) + r.Equal(MetricName(&types.NamespacedName{Namespace: ns, Name: t.Name()}), spec.MetricName) r.Equal(int64(123), spec.TargetSize) }, }, @@ -330,27 +341,32 @@ func TestGetMetricSpecTable(t *testing.T) { name: "valid hosts as multiple hosts value in scaler metadata", defaultTargetMetric: 0, defaultTargetMetricInterceptor: 123, - scalerMetadata: map[string]string{ - "hosts": "validHost1,validHost2", - "targetPendingRequests": "123", - }, - newRoutingTableFn: func() *routing.Table { - ret := routing.NewTable() - r.NoError(ret.AddTarget("validHost1", routing.NewTarget( - ns, - "testsrv", - 8080, - "testdepl", - 123, - ))) - r.NoError(ret.AddTarget("validHost2", routing.NewTarget( - ns, - "testsrv", - 8080, - "testdepl", - 123, - ))) - return ret + newInformer: func(t *testing.T, ctrl *gomock.Controller) *informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer { + informer, _, namespaceLister := newMocks(ctrl) + + httpso := &httpv1alpha1.HTTPScaledObject{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns, + Name: t.Name(), + }, + Spec: httpv1alpha1.HTTPScaledObjectSpec{ + Hosts: []string{ + "validHost1", + "validHost2", + }, + ScaleTargetRef: httpv1alpha1.ScaleTargetRef{ + Deployment: "testdepl", + Service: "testsrv", + Port: 8080, + }, + TargetPendingRequests: pointer.Int32(123), + }, + } + namespaceLister.EXPECT(). + Get(httpso.GetName()). + Return(httpso, nil) + + return informer }, checker: func(t *testing.T, res *externalscaler.GetMetricSpecResponse, err error) { t.Helper() @@ -359,40 +375,10 @@ func TestGetMetricSpecTable(t *testing.T) { r.NotNil(res) r.Equal(1, len(res.MetricSpecs)) spec := res.MetricSpecs[0] - r.Equal(httpRequests, spec.MetricName) + r.Equal(MetricName(&types.NamespacedName{Namespace: ns, Name: t.Name()}), spec.MetricName) r.Equal(int64(123), spec.TargetSize) }, }, - { - name: "interceptor as host in scaler metadata", - defaultTargetMetric: 1000, - defaultTargetMetricInterceptor: 2000, - scalerMetadata: map[string]string{ - "hosts": interceptor, - "targetPendingRequests": "123", - }, - newRoutingTableFn: func() *routing.Table { - ret := routing.NewTable() - r.NoError(ret.AddTarget("validHost", routing.NewTarget( - ns, - "testsrv", - 8080, - "testdepl", - 123, - ))) - return ret - }, - checker: func(t *testing.T, res *externalscaler.GetMetricSpecResponse, err error) { - t.Helper() - r := require.New(t) - r.NoError(err) - r.NotNil(res) - r.Equal(1, len(res.MetricSpecs)) - spec := res.MetricSpecs[0] - r.Equal(interceptor, spec.MetricName) - r.Equal(int64(2000), spec.TargetSize) - }, - }, } for i, c := range cases { @@ -401,10 +387,13 @@ func TestGetMetricSpecTable(t *testing.T) { // in parallel testCase := c t.Run(testName, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := context.Background() t.Parallel() lggr := logr.Discard() - table := testCase.newRoutingTableFn() + informer := testCase.newInformer(t, ctrl) ticker, pinger, err := newFakeQueuePinger(ctx, lggr) if err != nil { t.Fatalf( @@ -416,12 +405,13 @@ func TestGetMetricSpecTable(t *testing.T) { hdl := newImpl( lggr, pinger, - table, + informer, testCase.defaultTargetMetric, testCase.defaultTargetMetricInterceptor, ) scaledObjectRef := externalscaler.ScaledObjectRef{ - ScalerMetadata: testCase.scalerMetadata, + Namespace: ns, + Name: t.Name(), } ret, err := hdl.GetMetricSpec(ctx, &scaledObjectRef) testCase.checker(t, ret, err) @@ -430,13 +420,18 @@ func TestGetMetricSpecTable(t *testing.T) { } func TestGetMetrics(t *testing.T) { + const ( + ns = "default" + ) + type testCase struct { - name string - scalerMetadata map[string]string - setupFn func( + name string + setupFn func( + *testing.T, + *gomock.Controller, context.Context, logr.Logger, - ) (*routing.Table, *queuePinger, func(), error) + ) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *queuePinger, func(), error) checkFn func(*testing.T, *externalscaler.GetMetricsResponse, error) defaultTargetMetric int64 defaultTargetMetricInterceptor int64 @@ -489,77 +484,22 @@ func TestGetMetrics(t *testing.T) { testCases := []testCase{ { - name: "no 'hosts' field in the scaler metadata field", - scalerMetadata: map[string]string{}, + name: "HTTPSO missing in the queue pinger", setupFn: func( + t *testing.T, + ctrl *gomock.Controller, ctx context.Context, lggr logr.Logger, - ) (*routing.Table, *queuePinger, func(), error) { - table := routing.NewTable() - ticker, pinger, err := newFakeQueuePinger(ctx, lggr) - if err != nil { - return nil, nil, nil, err - } - return table, pinger, func() { ticker.Stop() }, nil - }, - checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) { - t.Helper() - r := require.New(t) - r.Error(err) - r.Nil(res) - r.Contains( - err.Error(), - "no 'hosts' field in the scaler metadata field", - ) - }, - defaultTargetMetric: int64(200), - defaultTargetMetricInterceptor: int64(300), - }, - { - name: "missing host value in the queue pinger", - scalerMetadata: map[string]string{ - "hosts": "missingHostInQueue", - }, - setupFn: func( - ctx context.Context, - lggr logr.Logger, - ) (*routing.Table, *queuePinger, func(), error) { - table := routing.NewTable() + ) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *queuePinger, func(), error) { + informer, _, _ := newMocks(ctrl) + // create queue and ticker without the host in it ticker, pinger, err := newFakeQueuePinger(ctx, lggr) if err != nil { return nil, nil, nil, err } - return table, pinger, func() { ticker.Stop() }, nil - }, - checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) { - t.Helper() - r := require.New(t) - r.Error(err) - r.Contains(err.Error(), "host 'missingHostInQueue' not found in counts") - r.Nil(res) - }, - defaultTargetMetric: int64(200), - defaultTargetMetricInterceptor: int64(300), - }, - { - name: "valid host", - scalerMetadata: map[string]string{ - "hosts": "validHost", - }, - setupFn: func( - ctx context.Context, - lggr logr.Logger, - ) (*routing.Table, *queuePinger, func(), error) { - table := routing.NewTable() - pinger, done, err := startFakeInterceptorServer(ctx, lggr, map[string]int{ - "validHost": 201, - }, 2*time.Millisecond) - if err != nil { - return nil, nil, nil, err - } - return table, pinger, done, nil + return informer, pinger, func() { ticker.Stop() }, nil }, checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) { t.Helper() @@ -568,70 +508,36 @@ func TestGetMetrics(t *testing.T) { r.NotNil(res) r.Equal(1, len(res.MetricValues)) metricVal := res.MetricValues[0] - r.Equal(httpRequests, metricVal.MetricName) - r.Equal(int64(201), metricVal.MetricValue) + r.Equal(MetricName(&types.NamespacedName{Namespace: ns, Name: t.Name()}), metricVal.MetricName) + r.Equal(int64(0), metricVal.MetricValue) }, defaultTargetMetric: int64(200), defaultTargetMetricInterceptor: int64(300), }, { - name: "'interceptor' as host", - scalerMetadata: map[string]string{ - "hosts": interceptor, - }, + name: "HTTPSO present in the queue pinger", setupFn: func( + t *testing.T, + ctrl *gomock.Controller, ctx context.Context, lggr logr.Logger, - ) (*routing.Table, *queuePinger, func(), error) { - table := routing.NewTable() - pinger, done, err := startFakeInterceptorServer(ctx, lggr, map[string]int{ - "host1": 201, - "host2": 202, - }, 2*time.Millisecond) - if err != nil { - return nil, nil, nil, err + ) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *queuePinger, func(), error) { + informer, _, _ := newMocks(ctrl) + + namespacedName := &types.NamespacedName{ + Namespace: ns, + Name: t.Name(), } - return table, pinger, done, nil - }, - checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) { - t.Helper() - r := require.New(t) - r.NoError(err) - r.NotNil(res) - r.Equal(1, len(res.MetricValues)) - metricVal := res.MetricValues[0] - r.Equal(interceptor, metricVal.MetricName) - // the value here needs to be the same thing as - // the sum of the values in the fake queue created - // in the setup function - r.Equal(int64(403), metricVal.MetricValue) - }, - defaultTargetMetric: int64(200), - defaultTargetMetricInterceptor: int64(300), - }, - { - name: "host in routing table, missing in queue pinger", - scalerMetadata: map[string]string{ - "hosts": "myhost.com", - }, - setupFn: func( - ctx context.Context, - lggr logr.Logger, - ) (*routing.Table, *queuePinger, func(), error) { - table := routing.NewTable() - r := require.New(t) - r.NoError(table.AddTarget( - "myhost.com", - standardTarget(), - )) + key := namespacedName.String() + pinger, done, err := startFakeInterceptorServer(ctx, lggr, map[string]int{ - "host1": 201, - "host2": 202, + key: 201, }, 2*time.Millisecond) if err != nil { return nil, nil, nil, err } - return table, pinger, done, nil + + return informer, pinger, done, nil }, checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) { t.Helper() @@ -640,34 +546,36 @@ func TestGetMetrics(t *testing.T) { r.NotNil(res) r.Equal(1, len(res.MetricValues)) metricVal := res.MetricValues[0] - r.Equal(httpRequests, metricVal.MetricName) - // the value here needs to be the same thing as - // the sum of the values in the fake queue created - // in the setup function - r.Equal(int64(0), metricVal.MetricValue) + r.Equal(MetricName(&types.NamespacedName{Namespace: ns, Name: t.Name()}), metricVal.MetricName) + r.Equal(int64(201), metricVal.MetricValue) }, defaultTargetMetric: int64(200), defaultTargetMetricInterceptor: int64(300), }, { name: "multiple validHosts add MetricValues", - scalerMetadata: map[string]string{ - "hosts": "validHost1,validHost2", - }, setupFn: func( + t *testing.T, + ctrl *gomock.Controller, ctx context.Context, lggr logr.Logger, - ) (*routing.Table, *queuePinger, func(), error) { - table := routing.NewTable() + ) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *queuePinger, func(), error) { + informer, _, _ := newMocks(ctrl) + + namespacedName := &types.NamespacedName{ + Namespace: ns, + Name: t.Name(), + } + key := namespacedName.String() + pinger, done, err := startFakeInterceptorServer(ctx, lggr, map[string]int{ - "validHost1": 123, - "validHost2": 456, + key: 579, }, 2*time.Millisecond) if err != nil { return nil, nil, nil, err } - return table, pinger, done, nil + return informer, pinger, done, nil }, checkFn: func(t *testing.T, res *externalscaler.GetMetricsResponse, err error) { t.Helper() @@ -676,7 +584,7 @@ func TestGetMetrics(t *testing.T) { r.NotNil(res) r.Equal(1, len(res.MetricValues)) metricVal := res.MetricValues[0] - r.Equal(httpRequests, metricVal.MetricName) + r.Equal(MetricName(&types.NamespacedName{Namespace: ns, Name: t.Name()}), metricVal.MetricName) // the value here needs to be the same thing as // the sum of the values in the fake queue created // in the setup function @@ -691,28 +599,56 @@ func TestGetMetrics(t *testing.T) { tc := c name := fmt.Sprintf("test case %d: %s", i, tc.name) t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + r := require.New(t) ctx, done := context.WithCancel( context.Background(), ) defer done() lggr := logr.Discard() - table, pinger, cleanup, err := tc.setupFn(ctx, lggr) + informer, pinger, cleanup, err := tc.setupFn(t, ctrl, ctx, lggr) r.NoError(err) defer cleanup() hdl := newImpl( lggr, pinger, - table, + informer, tc.defaultTargetMetric, tc.defaultTargetMetricInterceptor, ) res, err := hdl.GetMetrics(ctx, &externalscaler.GetMetricsRequest{ ScaledObjectRef: &externalscaler.ScaledObjectRef{ - ScalerMetadata: tc.scalerMetadata, + Namespace: ns, + Name: t.Name(), }, }) tc.checkFn(t, res, err) }) } } + +func newMocks(ctrl *gomock.Controller) (*informersexternalversionshttpv1alpha1mock.MockHTTPScaledObjectInformer, *listershttpv1alpha1mock.MockHTTPScaledObjectLister, *listershttpv1alpha1mock.MockHTTPScaledObjectNamespaceLister) { + namespaceLister := listershttpv1alpha1mock.NewMockHTTPScaledObjectNamespaceLister(ctrl) + namespaceLister.EXPECT(). + Get(""). + DoAndReturn(func(name string) (*httpv1alpha1.HTTPScaledObject, error) { + return nil, errors.NewNotFound(httpv1alpha1.Resource("httpscaledobject"), name) + }). + AnyTimes() + + lister := listershttpv1alpha1mock.NewMockHTTPScaledObjectLister(ctrl) + lister.EXPECT(). + HTTPScaledObjects(gomock.Any()). + Return(namespaceLister). + AnyTimes() + + informer := informersexternalversionshttpv1alpha1mock.NewMockHTTPScaledObjectInformer(ctrl) + informer.EXPECT(). + Lister(). + Return(lister). + AnyTimes() + + return informer, lister, namespaceLister +} diff --git a/scaler/host_counts_test.go b/scaler/host_counts_test.go deleted file mode 100644 index f8b1c345a..000000000 --- a/scaler/host_counts_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/kedacore/http-add-on/pkg/routing" -) - -type testCase struct { - name string - table routing.TableReader - counts map[string]int - retCounts map[string]int -} - -func cases(r *require.Assertions) []testCase { - return []testCase{ - { - name: "empty queue", - table: newRoutingTable(r, []hostAndTarget{ - { - host: "www.example.com", - target: routing.Target{}, - }, - { - host: "www.example2.com", - target: routing.Target{}, - }, - }), - counts: make(map[string]int), - retCounts: map[string]int{ - "www.example.com": 0, - "www.example2.com": 0, - }, - }, - { - name: "one entry in queue, same entry in routing table", - table: newRoutingTable(r, []hostAndTarget{ - { - host: "example.com", - target: routing.Target{}, - }, - }), - counts: map[string]int{ - "example.com": 1, - }, - retCounts: map[string]int{ - "example.com": 1, - }, - }, - { - name: "one entry in queue, two in routing table", - table: newRoutingTable(r, []hostAndTarget{ - { - host: "example.com", - target: routing.Target{}, - }, - { - host: "example2.com", - target: routing.Target{}, - }, - }), - counts: map[string]int{ - "example.com": 1, - }, - retCounts: map[string]int{ - "example.com": 1, - "example2.com": 0, - }, - }, - } -} -func TestGetHostCount(t *testing.T) { - r := require.New(t) - for _, tc := range cases(r) { - for host, retCount := range tc.retCounts { - t.Run(tc.name, func(t *testing.T) { - r := require.New(t) - ret, exists := getHostCount( - host, - tc.counts, - tc.table, - ) - r.True(exists) - r.Equal(retCount, ret) - }) - } - } -} - -type hostAndTarget struct { - host string - target routing.Target -} - -func newRoutingTable(r *require.Assertions, entries []hostAndTarget) *routing.Table { - ret := routing.NewTable() - for _, entry := range entries { - r.NoError(ret.AddTarget(entry.host, entry.target)) - } - return ret -} diff --git a/scaler/hosts.go b/scaler/hosts.go deleted file mode 100644 index 148f51b57..000000000 --- a/scaler/hosts.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/go-logr/logr" - - "github.com/kedacore/http-add-on/pkg/routing" - externalscaler "github.com/kedacore/http-add-on/proto" -) - -// getHostCount gets proper count for given host regardless whether -// host is in counts or only in routerTable -func getHostCount( - host string, - counts map[string]int, - table routing.TableReader, -) (int, bool) { - count, exists := counts[host] - if exists { - return count, exists - } - - exists = table.HasHost(host) - return 0, exists -} - -// gets hosts from scaledobjectref -func getHostsFromScaledObjectRef(lggr logr.Logger, sor *externalscaler.ScaledObjectRef) ([]string, error) { - serializedHosts, ok := sor.ScalerMetadata["hosts"] - if !ok { - err := fmt.Errorf("no 'hosts' field in the scaler metadata field") - lggr.Error(err, "'hosts' not found in the scaler metadata field") - return make([]string, 0), err - } - return strings.Split(serializedHosts, ","), nil -} diff --git a/scaler/main.go b/scaler/main.go index 04c577bd1..589e929ad 100644 --- a/scaler/main.go +++ b/scaler/main.go @@ -6,43 +6,43 @@ package main import ( "context" - "encoding/json" "fmt" "log" "net" - "net/http" "os" "time" "github.com/go-logr/logr" + "github.com/kedacore/keda/v2/pkg/scalers/externalscaler" "golang.org/x/sync/errgroup" "google.golang.org/grpc" + "google.golang.org/grpc/health" + "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" + "k8s.io/client-go/kubernetes" + ctrl "sigs.k8s.io/controller-runtime" + clientset "github.com/kedacore/http-add-on/operator/generated/clientset/versioned" + informers "github.com/kedacore/http-add-on/operator/generated/informers/externalversions" + informershttpv1alpha1 "github.com/kedacore/http-add-on/operator/generated/informers/externalversions/http/v1alpha1" "github.com/kedacore/http-add-on/pkg/build" - kedahttp "github.com/kedacore/http-add-on/pkg/http" "github.com/kedacore/http-add-on/pkg/k8s" pkglog "github.com/kedacore/http-add-on/pkg/log" - "github.com/kedacore/http-add-on/pkg/routing" - externalscaler "github.com/kedacore/http-add-on/proto" ) // +kubebuilder:rbac:groups="",namespace=keda,resources=configmaps,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=endpoints,verbs=get;list;watch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch +// +kubebuilder:rbac:groups=http.keda.sh,resources=httpscaledobjects,verbs=get;list;watch func main() { lggr, err := pkglog.NewZapr() if err != nil { log.Fatalf("error creating new logger (%v)", err) } - ctx, done := context.WithCancel( - context.Background(), - ) cfg := mustParseConfig() grpcPort := cfg.GRPCPort - healthPort := cfg.HealthPort namespace := cfg.TargetNamespace svcName := cfg.TargetService deplName := cfg.TargetDeployment @@ -50,10 +50,14 @@ func main() { targetPendingRequests := cfg.TargetPendingRequests targetPendingRequestsInterceptor := cfg.TargetPendingRequestsInterceptor - k8sCl, _, err := k8s.NewClientset() + k8sCfg, err := ctrl.GetConfig() + if err != nil { + lggr.Error(err, "Kubernetes client config not found") + os.Exit(1) + } + k8sCl, err := kubernetes.NewForConfig(k8sCfg) if err != nil { - lggr.Error(err, "getting a Kubernetes client") - done() + lggr.Error(err, "creating new Kubernetes ClientSet") os.Exit(1) } pinger, err := newQueuePinger( @@ -67,32 +71,9 @@ func main() { ) if err != nil { lggr.Error(err, "creating a queue pinger") - done() os.Exit(1) } - defer done() - - // This callback function is used to fetch and save - // the current queue counts from the interceptor immediately - // after updating the routingTable information. - callbackWhenRoutingTableUpdate := func() error { - if err := pinger.fetchAndSaveCounts(ctx); err != nil { - return err - } - return nil - } - - table := routing.NewTable() - // Create the informer of ConfigMap resource, - // the resynchronization period of the informer should be not less than 1s, - // refer to: https://github.com/kubernetes/client-go/blob/v0.22.2/tools/cache/shared_informer.go#L475 - configMapInformer := k8s.NewInformerConfigMapUpdater( - lggr, - k8sCl, - cfg.ConfigMapCacheRsyncPeriod, - cfg.TargetNamespace, - ) // create the deployment informer deployInformer := k8s.NewInformerBackedDeploymentCache( lggr, @@ -100,6 +81,19 @@ func main() { cfg.DeploymentCacheRsyncPeriod, ) + httpCl, err := clientset.NewForConfig(k8sCfg) + if err != nil { + lggr.Error(err, "creating new HTTP ClientSet") + os.Exit(1) + } + sharedInformerFactory := informers.NewSharedInformerFactory(httpCl, cfg.ConfigMapCacheRsyncPeriod) + httpsoInformer := informershttpv1alpha1.New(sharedInformerFactory, "", nil).HTTPScaledObjects() + + ctx, done := context.WithCancel( + context.Background(), + ) + defer done() + grp, ctx := errgroup.WithContext(ctx) // start the deployment informer @@ -108,6 +102,13 @@ func main() { return deployInformer.Start(ctx) }) + // start the httpso informer + grp.Go(func() error { + defer done() + httpsoInformer.Informer().Run(ctx.Done()) + return ctx.Err() + }) + grp.Go(func() error { defer done() return pinger.start( @@ -124,34 +125,12 @@ func main() { lggr, grpcPort, pinger, - table, + httpsoInformer, int64(targetPendingRequests), int64(targetPendingRequestsInterceptor), ) }) - grp.Go(func() error { - defer done() - return routing.StartConfigMapRoutingTableUpdater( - ctx, - lggr, - configMapInformer, - cfg.TargetNamespace, - table, - callbackWhenRoutingTableUpdate, - ) - }) - - grp.Go(func() error { - defer done() - return startAdminServer( - ctx, - lggr, - cfg, - healthPort, - pinger, - ) - }) build.PrintComponentInfo(lggr, "Scaler") lggr.Error(grp.Wait(), "one or more of the servers failed") } @@ -161,85 +140,44 @@ func startGrpcServer( lggr logr.Logger, port int, pinger *queuePinger, - routingTable *routing.Table, + httpsoInformer informershttpv1alpha1.HTTPScaledObjectInformer, targetPendingRequests int64, targetPendingRequestsInterceptor int64, ) error { addr := fmt.Sprintf("0.0.0.0:%d", port) lggr.Info("starting grpc server", "address", addr) + lis, err := net.Listen("tcp", addr) if err != nil { return err } grpcServer := grpc.NewServer() + reflection.Register(grpcServer) + + hs := health.NewServer() + hs.SetServingStatus("liveness", grpc_health_v1.HealthCheckResponse_SERVING) + hs.SetServingStatus("readiness", grpc_health_v1.HealthCheckResponse_SERVING) + grpc_health_v1.RegisterHealthServer( + grpcServer, + hs, + ) + externalscaler.RegisterExternalScalerServer( grpcServer, newImpl( lggr, pinger, - routingTable, + httpsoInformer, targetPendingRequests, targetPendingRequestsInterceptor, ), ) - reflection.Register(grpcServer) + go func() { <-ctx.Done() lis.Close() }() - return grpcServer.Serve(lis) -} - -func startAdminServer( - ctx context.Context, - lggr logr.Logger, - cfg *config, - port int, - pinger *queuePinger, -) error { - lggr = lggr.WithName("startHealthcheckServer") - mux := http.NewServeMux() - mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - }) - mux.HandleFunc("/livez", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - }) - mux.HandleFunc("/queue", func(w http.ResponseWriter, r *http.Request) { - lggr = lggr.WithName("route.counts") - cts := pinger.counts() - lggr.Info("counts endpoint", "counts", cts) - if err := json.NewEncoder(w).Encode(&cts); err != nil { - lggr.Error(err, "writing counts information to client") - w.WriteHeader(500) - } - }) - mux.HandleFunc("/queue_ping", func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - lggr := lggr.WithName("route.counts_ping") - if err := pinger.fetchAndSaveCounts(ctx); err != nil { - lggr.Error(err, "requesting counts failed") - w.WriteHeader(500) - _, err := w.Write([]byte("error requesting counts from interceptors")) - lggr.Error(err, "failed sending equesting counts failed") - return - } - cts := pinger.counts() - lggr.Info("counts ping endpoint", "counts", cts) - if err := json.NewEncoder(w).Encode(&cts); err != nil { - lggr.Error(err, "writing counts data to caller") - w.WriteHeader(500) - _, err := w.Write([]byte("error writing counts data to caller")) - lggr.Error(err, "failed sending writing counts data to caller") - } - }) - - kedahttp.AddConfigEndpoint(lggr, mux, cfg) - kedahttp.AddVersionEndpoint(lggr.WithName("scalerAdmin"), mux) - - addr := fmt.Sprintf("0.0.0.0:%d", port) - lggr.Info("starting health check server", "addr", addr) - return kedahttp.ServeContext(ctx, addr, mux) + return grpcServer.Serve(lis) } diff --git a/scaler/main_test.go b/scaler/main_test.go deleted file mode 100644 index ce7adddb5..000000000 --- a/scaler/main_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/go-logr/logr" - "github.com/stretchr/testify/require" - "golang.org/x/sync/errgroup" - "k8s.io/apimachinery/pkg/util/rand" -) - -func TestHealthChecks(t *testing.T) { - ctx := context.Background() - ctx, done := context.WithCancel(ctx) - defer done() - lggr := logr.Discard() - r := require.New(t) - port := rand.Intn(100) + 8000 - cfg := &config{ - GRPCPort: port + 1, - HealthPort: port, - TargetNamespace: "test123", - TargetService: "testsvc", - TargetPort: port + 123, - TargetPendingRequests: 100, - } - - errgrp, ctx := errgroup.WithContext(ctx) - - ticker, pinger, err := newFakeQueuePinger(ctx, lggr) - r.NoError(err) - defer ticker.Stop() - srvFunc := func() error { - return startAdminServer( - ctx, - lggr, - cfg, - port, - pinger, - ) - } - - newURL := func(path string) string { - return fmt.Sprintf("http://0.0.0.0:%d/%s", port, path) - } - errgrp.Go(srvFunc) - time.Sleep(500 * time.Millisecond) - - res, err := http.Get(newURL("healthz")) - r.NoError(err) - defer res.Body.Close() - r.Equal(200, res.StatusCode) - - res, err = http.Get(newURL("livez")) - r.NoError(err) - defer res.Body.Close() - r.Equal(200, res.StatusCode) - - res, err = http.Get(newURL("config")) - r.NoError(err) - defer res.Body.Close() - r.Equal(200, res.StatusCode) - bodyBytes, err := io.ReadAll(res.Body) - r.NoError(err) - retCfg := map[string][]config{} - r.NoError(json.Unmarshal(bodyBytes, &retCfg)) - expected := map[string][]config{ - "configs": {*cfg}, - } - r.Equal(expected, retCfg) - - done() - r.Error(errgrp.Wait()) -} diff --git a/scaler/naming.go b/scaler/naming.go new file mode 100644 index 000000000..28d5ad61d --- /dev/null +++ b/scaler/naming.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "regexp" + + "k8s.io/apimachinery/pkg/types" +) + +var ( + unsafeChars = regexp.MustCompile(`[^-.0-9A-Za-z]`) +) + +func escapeRune(r string) string { + return fmt.Sprintf("_%04X", r) +} + +func escapeString(s string) string { + return unsafeChars.ReplaceAllStringFunc(s, escapeRune) +} + +func MetricName(namespacedName *types.NamespacedName) string { + mn := fmt.Sprintf("http-%v", namespacedName) + return escapeString(mn) +} diff --git a/scaler/queue_pinger.go b/scaler/queue_pinger.go index d74463dcf..f69a5fb6e 100644 --- a/scaler/queue_pinger.go +++ b/scaler/queue_pinger.go @@ -14,7 +14,6 @@ import ( "github.com/kedacore/http-add-on/pkg/k8s" "github.com/kedacore/http-add-on/pkg/queue" - "github.com/kedacore/http-add-on/pkg/routing" ) // queuePinger has functionality to ping all interceptors @@ -128,29 +127,6 @@ func (q *queuePinger) counts() map[string]int { return q.allCounts } -// mergeCountsWithRoutingTable ensures that all hosts in routing table -// are present in combined counts, if count is not present value is set to 0 -func (q *queuePinger) mergeCountsWithRoutingTable( - table routing.TableReader, -) map[string]int { - q.pingMut.RLock() - defer q.pingMut.RUnlock() - mergedCounts := make(map[string]int) - for _, host := range table.Hosts() { - mergedCounts[host] = 0 - } - for key, value := range q.allCounts { - mergedCounts[key] = value - } - return mergedCounts -} - -func (q *queuePinger) aggregate() int { - q.pingMut.RLock() - defer q.pingMut.RUnlock() - return q.aggregateCount -} - // fetchAndSaveCounts calls fetchCounts, and then // saves them to internal state in q func (q *queuePinger) fetchAndSaveCounts(ctx context.Context) error { @@ -207,7 +183,7 @@ func fetchCounts( countsCh := make(chan *queue.Counts) var wg sync.WaitGroup - fetchGrp, ctx := errgroup.WithContext(ctx) + fetchGrp, _ := errgroup.WithContext(ctx) for _, endpoint := range endpointURLs { // capture the endpoint in a loop-local // variable so that the goroutine can @@ -220,8 +196,6 @@ func fetchCounts( wg.Add(1) fetchGrp.Go(func() error { counts, err := queue.GetCounts( - ctx, - lggr, http.DefaultClient, *u, ) diff --git a/scaler/queue_pinger_fake.go b/scaler/queue_pinger_fake.go deleted file mode 100644 index ef8428ba3..000000000 --- a/scaler/queue_pinger_fake.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - context "context" - "net/http" - "net/http/httptest" - "net/url" - "time" - - "github.com/go-logr/logr" - v1 "k8s.io/api/core/v1" - - "github.com/kedacore/http-add-on/pkg/k8s" - kedanet "github.com/kedacore/http-add-on/pkg/net" - "github.com/kedacore/http-add-on/pkg/queue" -) - -// startFakeQueuePinger starts a fake server that simulates -// an interceptor with its /queue endpoint, then returns a -// *v1.Endpoints object that contains the URL of the new fake -// server. also returns the *httptest.Server that runs the -// endpoint along with its URL. the caller is responsible for -// calling testServer.Close() when done. -// -// returns nil for the first 3 return value and a non-nil error in -// case of a failure. -func startFakeQueueEndpointServer( - svcName string, - q queue.CountReader, - numEndpoints int, -) (*httptest.Server, *url.URL, *v1.Endpoints, error) { - hdl := http.NewServeMux() - queue.AddCountsRoute(logr.Discard(), hdl, q) - srv, srvURL, err := kedanet.StartTestServer(hdl) - if err != nil { - return nil, nil, nil, err - } - endpoints, err := k8s.FakeEndpointsForURL(srvURL, "testns", svcName, numEndpoints) - if err != nil { - return nil, nil, nil, err - } - return srv, srvURL, endpoints, nil -} - -type fakeQueuePingerOpts struct { - endpoints *v1.Endpoints - tickDur time.Duration - port string -} - -type optsFunc func(*fakeQueuePingerOpts) - -// newFakeQueuePinger creates the machinery required for a fake -// queuePinger implementation, including a time.Ticker, then returns -// the ticker and the pinger. it is the caller's responsibility to -// call ticker.Stop() on the returned ticker. -func newFakeQueuePinger( - ctx context.Context, - lggr logr.Logger, - optsFuncs ...optsFunc, -) (*time.Ticker, *queuePinger, error) { - opts := &fakeQueuePingerOpts{ - endpoints: &v1.Endpoints{}, - tickDur: time.Second, - port: "8080", - } - for _, optsFunc := range optsFuncs { - optsFunc(opts) - } - ticker := time.NewTicker(opts.tickDur) - - pinger, err := newQueuePinger( - ctx, - lggr, - func(context.Context, string, string) (*v1.Endpoints, error) { - return opts.endpoints, nil - }, - "testns", - "testsvc", - "testdepl", - opts.port, - ) - if err != nil { - return nil, nil, err - } - return ticker, pinger, nil -} diff --git a/scaler/queue_pinger_test.go b/scaler/queue_pinger_test.go index 0b433c9bc..2159ed702 100644 --- a/scaler/queue_pinger_test.go +++ b/scaler/queue_pinger_test.go @@ -1,16 +1,19 @@ package main import ( - context "context" + "context" + "net/http" + "net/http/httptest" + "net/url" "testing" "time" "github.com/go-logr/logr" "github.com/stretchr/testify/require" - "golang.org/x/sync/errgroup" v1 "k8s.io/api/core/v1" "github.com/kedacore/http-add-on/pkg/k8s" + kedanet "github.com/kedacore/http-add-on/pkg/net" "github.com/kedacore/http-add-on/pkg/queue" ) @@ -217,48 +220,73 @@ func TestFetchCounts(t *testing.T) { r.Equal(expectedCounts, cts) } -func TestMergeCountsWithRoutingTable(t *testing.T) { - r := require.New(t) - for _, tc := range cases(r) { - t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - grp, ctx := errgroup.WithContext(ctx) - r := require.New(t) - const C = 100 - tickr, q, err := newFakeQueuePinger( - ctx, - logr.Discard(), - ) - r.NoError(err) - defer tickr.Stop() - q.allCounts = tc.counts +// startFakeQueuePinger starts a fake server that simulates +// an interceptor with its /queue endpoint, then returns a +// *v1.Endpoints object that contains the URL of the new fake +// server. also returns the *httptest.Server that runs the +// endpoint along with its URL. the caller is responsible for +// calling testServer.Close() when done. +// +// returns nil for the first 3 return value and a non-nil error in +// case of a failure. +func startFakeQueueEndpointServer( + svcName string, + q queue.CountReader, + numEndpoints int, +) (*httptest.Server, *url.URL, *v1.Endpoints, error) { + hdl := http.NewServeMux() + queue.AddCountsRoute(logr.Discard(), hdl, q) + srv, srvURL, err := kedanet.StartTestServer(hdl) + if err != nil { + return nil, nil, nil, err + } + endpoints, err := k8s.FakeEndpointsForURL(srvURL, "testns", svcName, numEndpoints) + if err != nil { + return nil, nil, nil, err + } + return srv, srvURL, endpoints, nil +} - retCh := make(chan map[string]int) - for i := 0; i < C; i++ { - grp.Go(func() error { - retCh <- q.mergeCountsWithRoutingTable(tc.table) - return nil - }) - } +type fakeQueuePingerOpts struct { + endpoints *v1.Endpoints + tickDur time.Duration + port string +} - // ensure we receive from retCh C times - allRets := map[int]map[string]int{} - for i := 0; i < C; i++ { - allRets[i] = <-retCh - } +type optsFunc func(*fakeQueuePingerOpts) - r.NoError(grp.Wait()) +// newFakeQueuePinger creates the machinery required for a fake +// queuePinger implementation, including a time.Ticker, then returns +// the ticker and the pinger. it is the caller's responsibility to +// call ticker.Stop() on the returned ticker. +func newFakeQueuePinger( + ctx context.Context, + lggr logr.Logger, + optsFuncs ...optsFunc, +) (*time.Ticker, *queuePinger, error) { + opts := &fakeQueuePingerOpts{ + endpoints: &v1.Endpoints{}, + tickDur: time.Second, + port: "8080", + } + for _, optsFunc := range optsFuncs { + optsFunc(opts) + } + ticker := time.NewTicker(opts.tickDur) - // ensure that all returned maps are the - // same - prev := allRets[0] - for i := 1; i < C; i++ { - r.Equal(prev, allRets[i]) - prev = allRets[i] - } - // ensure that all the returned maps are - // equal to what we expected - r.Equal(tc.retCounts, prev) - }) + pinger, err := newQueuePinger( + ctx, + lggr, + func(context.Context, string, string) (*v1.Endpoints, error) { + return opts.endpoints, nil + }, + "testns", + "testsvc", + "testdepl", + opts.port, + ) + if err != nil { + return nil, nil, err } + return ticker, pinger, nil }