Skip to content

Commit a394d3c

Browse files
authored
feat!: rewrite controller (#396)
1 parent 7ecb10e commit a394d3c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2373
-1217
lines changed

.github/workflows/main.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Setup Go
2121
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
2222
with:
23-
go-version: 1.24.x
23+
go-version: 1.25.x
2424
- name: Tests
2525
run: make test
2626
- name: Send go coverage report

.github/workflows/pr-build.yaml

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
- name: Setup Go
5757
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
5858
with:
59-
go-version: 1.24.x
59+
go-version: 1.25.x
6060
- name: fmt
6161
run: make fmt
6262
- name: vet
@@ -76,10 +76,10 @@ jobs:
7676
strategy:
7777
matrix:
7878
kubernetes-version:
79-
- "1.27"
80-
- "1.28"
81-
- "1.29"
82-
- "1.30"
79+
- "1.31"
80+
- "1.32"
81+
- "1.33"
82+
- "1.34"
8383
steps:
8484
- name: Harden Runner
8585
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
@@ -90,7 +90,7 @@ jobs:
9090
- name: Setup Go
9191
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
9292
with:
93-
go-version: 1.24.x
93+
go-version: 1.25.x
9494
- name: run test
9595
run: make test ENVTEST_K8S_VERSION=${{ matrix.kubernetes-version }}
9696

@@ -108,7 +108,7 @@ jobs:
108108
- name: Setup Go
109109
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
110110
with:
111-
go-version: 1.24.x
111+
go-version: 1.25.x
112112
- name: build
113113
run: make build
114114
- name: Check if working tree is dirty
@@ -136,51 +136,6 @@ jobs:
136136
echo $profiles
137137
echo "::set-output name=matrix::$profiles"
138138
139-
e2e-tests:
140-
runs-on: ubuntu-latest
141-
needs:
142-
- build
143-
strategy:
144-
matrix:
145-
profile: ${{ fromJson(needs.build.outputs.profiles) }}
146-
steps:
147-
- name: Harden Runner
148-
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
149-
with:
150-
egress-policy: audit
151-
- name: Checkout
152-
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
153-
- name: Setup Go
154-
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
155-
with:
156-
go-version: 1.24.x
157-
- name: Setup Kubernetes
158-
uses: engineerd/setup-kind@aa272fe2a7309878ffc2a81c56cfe3ef108ae7d0 #v0.5.0
159-
with:
160-
version: v0.17.0
161-
- name: Download webhook-controller container
162-
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
163-
with:
164-
name: webhook-controller-container
165-
path: /tmp
166-
- name: Load images
167-
run: |
168-
docker load --input /tmp/webhook-controller-container.tar
169-
docker image ls -a
170-
- name: Setup Kustomize
171-
uses: imranismail/setup-kustomize@2ba527d4d055ab63514ba50a99456fc35684947f # v2.1.0
172-
- name: Run test
173-
run: |
174-
make kind-test TEST_PROFILE=${{ matrix.profile }}
175-
- name: Debug failure
176-
if: failure()
177-
run: |
178-
kubectl -n kube-system describe pods
179-
kubectl -n webhook-system describe pods
180-
kubectl -n webhook-system get all
181-
kubectl -n webhook-system logs deploy/webhook-controller
182-
kubectl -n webhook-system get webhookinstance -o yaml
183-
184139
test-chart:
185140
runs-on: ubuntu-latest
186141
needs:
@@ -227,6 +182,6 @@ jobs:
227182

228183
test-success:
229184
runs-on: ubuntu-latest
230-
needs: [test, e2e-tests]
185+
needs: [test]
231186
steps:
232187
- run: echo "all tests succeeded"

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
fetch-depth: 0
2525
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
2626
with:
27-
go-version: '1.24.x'
27+
go-version: '1.25.x'
2828
- name: Docker Login
2929
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
3030
with:

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ help: ## Display this help.
4141

4242
.PHONY: manifests
4343
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
44-
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/base/crd/bases
44+
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:rbac:artifacts:config=config/base/rbac output:crd:artifacts:config=config/base/crd/bases
4545
cp config/base/crd/bases/* chart/webhook-controller/crds/
4646

4747
.PHONY: generate
@@ -134,7 +134,7 @@ undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/confi
134134
CONTROLLER_GEN = $(GOBIN)/controller-gen
135135
.PHONY: controller-gen
136136
controller-gen: ## Download controller-gen locally if necessary.
137-
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.1)
137+
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.18.0)
138138

139139
GOLANGCI_LINT = $(GOBIN)/golangci-lint
140140
.PHONY: golangci-lint
@@ -149,7 +149,7 @@ kustomize: ## Download kustomize locally if necessary.
149149
ENVTEST = $(GOBIN)/setup-envtest
150150
.PHONY: envtest
151151
envtest: ## Download envtest-setup locally if necessary.
152-
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@release-0.17)
152+
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@release-0.22)
153153

154154
# go-install-tool will 'go install' any package $2 and install it to $1
155155
define go-install-tool

PROJECT

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ resources:
99
namespaced: true
1010
controller: true
1111
domain: doodle.com
12-
group: proxy.infra.doodle.com
13-
kind: RequestClone
12+
group: webhook.infra.doodle.com
13+
kind: Receiver
1414
path: github.com/doodlescheduling/webhook-controller/api/v1beta1
1515
version: v1beta1
1616
version: "3"

README.md

Lines changed: 152 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,56 +8,179 @@
88
[![license](https://img.shields.io/github/license/DoodleScheduling/webhook-controller.svg)](https://github.com/DoodleScheduling/webhook-controller/blob/master/LICENSE)
99

1010
This HTTP proxy duplicates incoming requests and sends concurrently to multiple targets.
11-
The response will be HTTP 202 Accepted if at least one matching target was found. The responses from the targets are not exposed
12-
to upstream by design.
11+
The response is asynchronous by default `HTTP 202 Accepted` if at least one matching target was found.
12+
However alternatively synchronous processing is also supported (see bellow).
1313

1414
## Why?
1515
This proxy is especially useful for handling incoming webhooks which need to be distributed to multiple targets.
1616
However it can be used for any other use case where a request needs to be duplicated.
1717

18-
## Example RequestClone
18+
## Setup
19+
20+
The proxy should not be exposed directly to the public. Rather should traffic be routed via an ingress controller
21+
to the webhook-controller.
1922

20-
These two targets both match `webhook-receiver.example.com`, meaning any incoming request will be sent to both endpoints.
21-
In this case to `webhook-receiver-app-1:80` and `webhook-receiver-app-2:80`.
23+
## Example Receiver
24+
25+
In this example an incoming http request will be cloned and forwarded to `podinfo:http` and `podinfo-v2:9091`.
26+
The response will be `HTTP 202 Accepted` no matter what these targets will respond.
2227

2328
```yaml
24-
apiVersion: proxy.infra.doodle.com/v1beta1
25-
kind: RequestClone
29+
apiVersion: webhook.infra.doodle.com/v1beta1
30+
kind: Receiver
2631
metadata:
2732
name: webhook-receiver
28-
namespace: apps
2933
spec:
30-
host: webhook-receiver.example.com
31-
backend:
32-
serviceName: webhook-receiver-app-1
33-
servicePort: http
34-
---
35-
apiVersion: proxy.infra.doodle.com/v1beta1
36-
kind: RequestClone
34+
targets:
35+
- service:
36+
name: podinfo
37+
port:
38+
name: http
39+
- service:
40+
name: podinfo-v2
41+
port:
42+
number: 9091
43+
```
44+
45+
Once the controller reconciled the receiver it will be registered in the proxy which by default processes incoming requests at port `8080`.
46+
Each receiver will receive its own dedicated http path to which webhooks can be send to. See `.status.webhookPath`.
47+
Usually the webhook-controller is exposed via an ingress. In this case the the webhooks must sent to `http://ingress-host/hooks/ixuxbmoofkiq9s2l61h6i2sl6hdgwnud`.
48+
**Note**: The webhookPath will not change anymore once it was set.
49+
50+
```
51+
apiVersion: webhook.infra.doodle.com/v1beta1
52+
kind: Receiver
3753
metadata:
3854
name: webhook-receiver
39-
namespace: apps
4055
spec:
41-
host: webhook-receiver.example.com
42-
backend:
43-
serviceName: webhook-receiver-app-2
44-
servicePort: http
56+
type: Async
57+
targets:
58+
- service:
59+
name: podinfo
60+
port:
61+
name: http
62+
- service:
63+
name: podinfo-v2
64+
port:
65+
name: http
66+
spec:
67+
responseType: Async
68+
targets:
69+
- service:
70+
name: podinfo
71+
port:
72+
name: http
73+
status:
74+
conditions:
75+
- lastTransitionTime: "2025-12-15T07:41:02Z"
76+
message: receiver successfully registered
77+
reason: ServiceBackendReady
78+
status: "True"
79+
type: Ready
80+
webhookPath: /hooks/ixuxbmoofkiq9s2l61h6i2sl6hdgwnud
81+
```
82+
83+
## More configurations
4584
85+
### Response type
86+
Besides async responses a receiver can also be synchronous. Meaning it will await the upstream responses.
87+
In this case `AwaitAllPreferSuccessful` will wait for both upstream targets and will send downstream the first successful http response from either targets once all targets
88+
were processed.
89+
90+
```yaml
91+
apiVersion: webhook.infra.doodle.com/v1beta1
92+
kind: Receiver
93+
metadata:
94+
name: webhook-receiver
95+
spec:
96+
type: AwaitAllPreferSuccessful
97+
targets:
98+
- service:
99+
name: podinfo
100+
port:
101+
name: http
102+
- service:
103+
name: podinfo-v2
104+
port:
105+
number: 9091
46106
```
47-
North south routing looks like this:
107+
108+
The following types are supported:
109+
* `Async` - The default, does not await reponses from upstream and immeadiately acknowledges incoming requests with a `HTTP 202 Accepted`.
110+
* `AwaitAllPreferSuccessful` - Await all upstream responses and send back the first successful repsonse (>= 200 && < 400). If all of them are not successful it will send back the first error response.
111+
* `AwaitAllPreferFailed` - Await all upstream responses and send back the first failed repsonse (< 200 && >= 400). If all of them are successful it will send back the first sucessful response.
112+
* `AwaitAllReport` - Await all upstream responses and send back a json object containing all target responses including status code, body and headers. The status code of this type will always be `HTTP 200 OK`.
113+
114+
### Path rewrite
115+
116+
By default http requests are sent upstream to `/`. The target path can be rewritten like:
117+
118+
```yaml
119+
apiVersion: webhook.infra.doodle.com/v1beta1
120+
kind: Receiver
121+
metadata:
122+
name: webhook-receiver
123+
spec:
124+
timeout: 3s
125+
targets:
126+
- path: /new/path
127+
service:
128+
name: podinfo
129+
port:
130+
name: http
131+
- path: /another/path
132+
service:
133+
name: podinfo-v2
134+
port:
135+
number: 9091
48136
```
49-
50-
=> Ingress controller proxy => => webhook-receiver-app-1:80
51-
client webhook
52-
[webhook-receiver.example.com] <= <= => webhook-receiver-app-2:80
53-
202 Accepted
137+
138+
### Timeout
139+
140+
The default timeout for upstream requests is `10s`, this can be changed however:
141+
142+
```yaml
143+
apiVersion: webhook.infra.doodle.com/v1beta1
144+
kind: Receiver
145+
metadata:
146+
name: webhook-receiver
147+
spec:
148+
timeout: 3s
149+
targets:
150+
- service:
151+
name: podinfo
152+
port:
153+
name: http
154+
- service:
155+
name: podinfo-v2
156+
port:
157+
number: 9091
54158
```
55159

160+
### Cross namespace targets
56161

57-
## Setup
162+
By default target services are only selected in the same namespace the receiver lives. A receiver can discover services across namespaces by defining a namespace selector on the target. In this case a service called `podinfo` will be disovered in any namespace on the cluster.
58163

59-
The proxy should not be exposed directly to the public. Rather should traffic be routed via an ingress controller
60-
and only hosts which are used to duplicate requests should be routed via this proxy.
164+
```yaml
165+
apiVersion: webhook.infra.doodle.com/v1beta1
166+
kind: Receiver
167+
metadata:
168+
name: webhook-receiver
169+
spec:
170+
type: AwaitAllPreferSuccessful
171+
targets:
172+
- service:
173+
name: podinfo
174+
port:
175+
name: http
176+
namespaceSelector: {}
177+
178+
```
179+
180+
### OpenTelemetry distributed tracing
181+
The controller supports http traces for the requests. See the `--otel-*` controller flags bellow.
182+
183+
## Installation
61184

62185
### Helm chart
63186

@@ -95,8 +218,6 @@ The controller can be configured using cmd args:
95218
--otel-tls-client-cert-path string Opentelemetry gRPC mTLS client cert path
96219
--otel-tls-client-key-path string Opentelemetry gRPC mTLS client key path
97220
--otel-tls-root-ca-path string Opentelemetry gRPC mTLS root CA path
98-
--proxy-read-timeout duration Read timeout for proxy requests. (default 10s)
99-
--proxy-write-timeout duration Write timeout for proxy requests. (default 10s)
100221
--watch-all-namespaces Watch for resources in all namespaces, if set to false it will only watch the runtime namespace. (default true)
101222
--watch-label-selector string Watch for resources with matching labels e.g. 'sharding.fluxcd.io/shard=shard1'.
102223
```

api/v1beta1/groupversion_info.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
// Package v1beta1 contains API Schema definitions for the request.infra.doodle.com v1beta1 API group
17+
// Package v1beta1 contains API Schema definitions for the webhook.infra.doodle.com v1beta1 API group
1818
// +kubebuilder:object:generate=true
19-
// +groupName=proxy.infra.doodle.com
19+
// +groupName=webhook.infra.doodle.com
2020
package v1beta1
2121

2222
import (
@@ -26,7 +26,7 @@ import (
2626

2727
var (
2828
// GroupVersion is group version used to register these objects
29-
GroupVersion = schema.GroupVersion{Group: "proxy.infra.doodle.com", Version: "v1beta1"}
29+
GroupVersion = schema.GroupVersion{Group: "webhook.infra.doodle.com", Version: "v1beta1"}
3030

3131
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
3232
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

0 commit comments

Comments
 (0)