Skip to content

Commit 3fc4f5c

Browse files
committed
implement kafka consumer for noobaa events
1 parent fe13fb6 commit 3fc4f5c

12 files changed

Lines changed: 376 additions & 44 deletions

File tree

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in
216216
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
217217
$(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
218218

219+
.PHONY: deploy-kafka
220+
deploy-kafka: manifests kustomize ## Deploy controller in Kafka notifications mode to the K8s cluster specified in ~/.kube/config.
221+
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
222+
$(KUSTOMIZE) build config/kafka | $(KUBECTL) apply -f -
223+
219224
.PHONY: undeploy
220225
undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
221226
$(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -

PLAN

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ oc patch noobaa noobaa --type='merge' -n openshift-storage -p '{
5353
```
5454

5555
The adapter's deployment should be configured with environment variables that
56-
identify its connection name (see NOOBAA_CONNECTION_NAME in the Kafka
57-
configuration section). When the operator calls put-bucket-notification for a
58-
bucket, it uses this connection name so that NooBaa knows where to deliver
59-
notifications (HTTP endpoint or Kafka topic, depending on NOTIFICATIONS_MODE).
56+
identify its connection name (ADAPTER_ID and ADAPTER_TOPIC). When the operator
57+
calls put-bucket-notification for a bucket, it uses these values so that NooBaa
58+
knows where to deliver notifications (HTTP endpoint or Kafka topic, depending on
59+
NOTIFICATIONS_MODE).
6060

6161
Internal MCG/NooBaa trigger CRD
6262
-------------------------------
@@ -166,22 +166,19 @@ and use those connection details and keys to issue the request to noobaa using S
166166
```
167167
aws s3api put-bucket-notification --bucket "foo-bucket" --notification-configuration '{
168168
"TopicConfiguration": {
169-
"Id": "$NOOBAA_CONNECTION_NAME",
169+
"Id": "$ADAPTER_ID",
170170
"Events": ["s3:ObjectCreated:*"],
171-
"Topic": "$NOOBAA_CONNECTION_NAME/connect.json"
171+
"Topic": "$ADAPTER_TOPIC"
172172
}
173173
}'
174174
```
175175

176176
The Topic in the S3 notification configuration is always a reference to the
177-
NooBaa connection secret: "NOOBAA_CONNECTION_NAME/connect.json". It is not the
178-
Kafka topic itself — for Kafka mode, the actual Kafka topic is the "topic" field
179-
inside the connect.json (see the NooBaa Kafka connection setup section). For HTTP
180-
mode, the connect.json contains the HTTP endpoint details instead.
181-
182-
NOOBAA_CONNECTION_NAME is an env var on the adapter deployment that identifies
183-
which NooBaa connection was configured during the one-time setup (HTTP or Kafka,
184-
depending on NOTIFICATIONS_MODE).
177+
NooBaa connection secret (ADAPTER_TOPIC, e.g. "mcg-adapter-connection/connect.json").
178+
It is not the Kafka topic itself — for Kafka mode, the actual Kafka topic is the
179+
"topic" field inside the connect.json (see the NooBaa Kafka connection setup
180+
section). For HTTP mode, the connect.json contains the HTTP endpoint details
181+
instead.
185182

186183
If successful, the BucketNotificationSet condition will be marked as "True"
187184

@@ -404,14 +401,14 @@ oc patch noobaa noobaa --type='merge' -n openshift-storage -p '{
404401
}'
405402
```
406403

407-
4. When the operator calls put-bucket-notification for a bucket, it uses the
408-
Kafka connection name as the Topic prefix, e.g.:
404+
4. When the operator calls put-bucket-notification for a bucket, it uses
405+
ADAPTER_ID and ADAPTER_TOPIC, e.g.:
409406

410407
```
411408
aws s3api put-bucket-notification --bucket "foo-bucket" \
412409
--notification-configuration '{
413410
"TopicConfiguration": {
414-
"Id": "mcg-adapter-connection",
411+
"Id": "mcg-adapter",
415412
"Events": ["s3:ObjectCreated:*"],
416413
"Topic": "mcg-adapter-connection/connect.json"
417414
}
@@ -424,14 +421,14 @@ Kafka configuration
424421

425422
Kafka configuration is defined via env on the adapter Deployment:
426423

424+
ADAPTER_ID: identifier for bucket notification configuration
425+
(default: "mcg-adapter")
426+
ADAPTER_TOPIC: NooBaa connection secret reference used as TopicArn in
427+
put-bucket-notification calls
428+
(default: "mcg-adapter-connection/connect.json")
427429
NOTIFICATIONS_MODE: "http" or "kafka" — selects whether the adapter
428430
receives NooBaa bucket notifications via its HTTP server or by
429431
consuming from a Kafka topic (default: "http")
430-
NOOBAA_CONNECTION_NAME: the name of the NooBaa connection configured
431-
during the one-time setup (e.g. "mcg-adapter-connection"). Used as
432-
the Id and Topic prefix in put-bucket-notification calls. The same
433-
name is used regardless of notifications mode — only one mode is
434-
active at a time.
435432
KAFKA_BROKERS: comma-separated list of host:port (required in both
436433
modes — even in HTTP mode the adapter needs Kafka to produce
437434
cloudevents to trigger Kafka topics)

README.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A Kubernetes operator that receives S3 bucket notifications from NooBaa (Multicl
44

55
## Description
66

7-
The MCG Adapter runs as an HTTP server inside a Kubernetes cluster, registered as a `bucketNotifications` connection in NooBaa. It watches `MCGOBCTrigger` custom resources to determine which S3 events from which buckets should be forwarded to which endpoints. When NooBaa delivers a notification, the adapter matches the event against all configured triggers and dispatches CloudEvents via HTTP or Kafka to the matching endpoints.
7+
The MCG Adapter runs inside a Kubernetes cluster, registered as a `bucketNotifications` connection in NooBaa. It can receive notifications via HTTP (default) or by consuming from a Kafka topic. It watches `MCGOBCTrigger` custom resources to determine which S3 events from which buckets should be forwarded to which endpoints. When NooBaa delivers a notification, the adapter matches the event against all configured triggers and dispatches CloudEvents via HTTP or Kafka to the matching endpoints.
88

99
## Custom Resource: MCGOBCTrigger
1010

@@ -54,13 +54,17 @@ The adapter is configured via environment variables:
5454
| Variable | Default | Description |
5555
|---|---|---|
5656
| `ADAPTER_ID` | `mcg-adapter` | Identifier used in the S3 bucket notification configuration |
57-
| `ADAPTER_TOPIC` | `mcg-adapter-connection` | Topic/connection name registered in NooBaa |
58-
| `ADAPTER_PORT` | `8888` | Port the notification HTTP server listens on |
59-
| `KAFKA_BROKERS` | _(none)_ | Comma-separated list of Kafka broker addresses (e.g. `broker1:9092,broker2:9092`). Required when using Kafka triggers. |
57+
| `ADAPTER_TOPIC` | `mcg-adapter-connection/connect.json` | NooBaa connection secret reference used as TopicArn in put-bucket-notification calls |
58+
| `ADAPTER_PORT` | `8888` | Port the notification HTTP server listens on (HTTP mode only) |
59+
| `NOTIFICATIONS_MODE` | `http` | `http` or `kafka` — selects how the adapter receives NooBaa notifications |
60+
| `KAFKA_BROKERS` | _(none)_ | Comma-separated list of Kafka broker addresses. Required for Kafka triggers and when `NOTIFICATIONS_MODE=kafka`. |
61+
| `KAFKA_SECRET` | _(none)_ | Name of a Kubernetes Secret (in the adapter's namespace) containing Kafka credentials. See `kafka-secret-format.md`. |
62+
| `KAFKA_NOTIFICATIONS_TOPIC` | _(none)_ | Kafka topic to consume NooBaa notifications from. Required when `NOTIFICATIONS_MODE=kafka`. |
63+
| `KAFKA_NOTIFICATIONS_GROUP_ID` | _(none)_ | Consumer group ID for consuming NooBaa notifications. Required when `NOTIFICATIONS_MODE=kafka`. |
6064

61-
### NooBaa Connection Setup
65+
### NooBaa Connection Setup (HTTP mode)
6266

63-
Before the adapter can receive notifications, register it as an HTTP connection in NooBaa (one-time setup):
67+
Before the adapter can receive notifications via HTTP, register it as an HTTP connection in NooBaa (one-time setup):
6468

6569
1. Create the connection secret:
6670

@@ -98,6 +102,17 @@ oc patch noobaa noobaa --type=’merge’ -n openshift-storage -p ‘{
98102
}’
99103
```
100104

105+
### NooBaa Connection Setup (Kafka mode)
106+
107+
As an alternative to HTTP, the adapter can consume NooBaa notifications from a Kafka topic. This requires a Strimzi-managed Kafka cluster. See the `PLAN` file for detailed setup steps, including creating the KafkaTopic, KafkaUsers, and the NooBaa Kafka connection secret.
108+
109+
Summary of the one-time setup:
110+
111+
1. Create a `KafkaTopic` (e.g. `mcg-adapter-notifications`) and `KafkaUser` resources in Strimzi
112+
2. Create a NooBaa connection secret with `notification_protocol: kafka` pointing to the Kafka bootstrap and topic
113+
3. Patch the NooBaa CR to register the Kafka connection
114+
4. Create a Kubernetes Secret in the adapter’s namespace with the adapter’s Kafka credentials (see `kafka-secret-format.md`)
115+
101116
### Deploy
102117

103118
```sh
@@ -106,6 +121,14 @@ make install
106121
make deploy IMG=<some-registry>/mcg-adapter:tag
107122
```
108123

124+
To deploy in Kafka notifications mode (using default values from the PLAN):
125+
126+
```sh
127+
make deploy-kafka IMG=<some-registry>/mcg-adapter:tag
128+
```
129+
130+
The `deploy-kafka` target uses the kustomize overlay in `config/kafka/` which sets `NOTIFICATIONS_MODE=kafka` along with default Kafka broker, topic, and secret values. Edit `config/kafka/kafka_env_patch.yaml` to customize these for your cluster.
131+
109132
### Run Locally (development)
110133

111134
```sh

cmd/main.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"crypto/tls"
2222
"flag"
23+
"fmt"
2324
"os"
2425
"path/filepath"
2526
"strconv"
@@ -228,6 +229,18 @@ func main() {
228229
os.Exit(1)
229230
}
230231
}
232+
notificationsMode := os.Getenv("NOTIFICATIONS_MODE")
233+
if notificationsMode == "" {
234+
notificationsMode = "http"
235+
}
236+
if notificationsMode != "http" && notificationsMode != "kafka" {
237+
setupLog.Error(fmt.Errorf("invalid NOTIFICATIONS_MODE %q", notificationsMode), "must be \"http\" or \"kafka\"")
238+
os.Exit(1)
239+
}
240+
241+
kafkaNotificationsTopic := os.Getenv("KAFKA_NOTIFICATIONS_TOPIC")
242+
kafkaNotificationsGroupID := os.Getenv("KAFKA_NOTIFICATIONS_GROUP_ID")
243+
231244
var kafkaBrokers []string
232245
if brokersStr := os.Getenv("KAFKA_BROKERS"); brokersStr != "" {
233246
kafkaBrokers = strings.Split(brokersStr, ",")
@@ -280,11 +293,29 @@ func main() {
280293
}
281294
// +kubebuilder:scaffold:builder
282295

296+
if notificationsMode == "kafka" {
297+
if kafkaNotificationsTopic == "" {
298+
setupLog.Error(fmt.Errorf("KAFKA_NOTIFICATIONS_TOPIC is required when NOTIFICATIONS_MODE=kafka"), "missing env")
299+
os.Exit(1)
300+
}
301+
if kafkaNotificationsGroupID == "" {
302+
setupLog.Error(fmt.Errorf("KAFKA_NOTIFICATIONS_GROUP_ID is required when NOTIFICATIONS_MODE=kafka"), "missing env")
303+
os.Exit(1)
304+
}
305+
if len(kafkaBrokers) == 0 {
306+
setupLog.Error(fmt.Errorf("KAFKA_BROKERS is required when NOTIFICATIONS_MODE=kafka"), "missing env")
307+
os.Exit(1)
308+
}
309+
}
310+
283311
notifServer := &notificationserver.NotificationServer{
284-
Client: mgr.GetClient(),
285-
Port: adapterPort,
286-
KafkaBrokers: kafkaBrokers,
287-
KafkaConfig: kafkaCfg,
312+
Client: mgr.GetClient(),
313+
Port: adapterPort,
314+
KafkaBrokers: kafkaBrokers,
315+
KafkaConfig: kafkaCfg,
316+
NotificationsMode: notificationsMode,
317+
KafkaNotificationsTopic: kafkaNotificationsTopic,
318+
KafkaNotificationsGroupID: kafkaNotificationsGroupID,
288319
}
289320
if err := mgr.Add(notifServer); err != nil {
290321
setupLog.Error(err, "unable to add notification server")

config/kafka/kafka_env_patch.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: controller-manager
5+
spec:
6+
template:
7+
spec:
8+
containers:
9+
- name: manager
10+
env:
11+
- name: NOTIFICATIONS_MODE
12+
value: "kafka"
13+
- name: KAFKA_BROKERS
14+
value: "my-cluster-kafka-bootstrap.kafka.svc:9095"
15+
- name: KAFKA_SECRET
16+
value: "mcg-adapter-user"
17+
- name: KAFKA_NOTIFICATIONS_TOPIC
18+
value: "mcg-adapter-notifications"
19+
- name: KAFKA_NOTIFICATIONS_GROUP_ID
20+
value: "mcg-adapter-notifications"

config/kafka/kustomization.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
resources:
2+
- ../default
3+
4+
patches:
5+
- path: kafka_env_patch.yaml
6+
target:
7+
kind: Deployment

config/manager/kustomization.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
55
kind: Kustomization
66
images:
77
- name: controller
8-
newName: example.com/mcg-adapter
9-
newTag: v0.0.1
8+
newName: quay.io/maschmid/mcg-adapter
9+
newTag: latest

config/manager/manager.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ spec:
7272
value: "mcg-adapter-connection/connect.json"
7373
- name: ADAPTER_PORT
7474
value: "8888"
75+
- name: NOTIFICATIONS_MODE
76+
value: "http"
7577
ports:
7678
- containerPort: 8888
7779
name: notifications
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package notificationserver
2+
3+
import (
4+
"github.com/IBM/sarama"
5+
)
6+
7+
type consumerGroupHandler struct {
8+
handler *notificationHandler
9+
}
10+
11+
func (h *consumerGroupHandler) Setup(_ sarama.ConsumerGroupSession) error {
12+
log.Info("kafka consumer group session setup")
13+
return nil
14+
}
15+
16+
func (h *consumerGroupHandler) Cleanup(_ sarama.ConsumerGroupSession) error {
17+
log.Info("kafka consumer group session cleanup")
18+
return nil
19+
}
20+
21+
func (h *consumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
22+
for msg := range claim.Messages() {
23+
log.Info("received kafka notification",
24+
"topic", msg.Topic,
25+
"partition", msg.Partition,
26+
"offset", msg.Offset)
27+
28+
if err := h.handler.processNotification(session.Context(), msg.Value); err != nil {
29+
log.Error(err, "processing kafka notification",
30+
"topic", msg.Topic,
31+
"partition", msg.Partition,
32+
"offset", msg.Offset)
33+
}
34+
35+
session.MarkMessage(msg, "")
36+
}
37+
return nil
38+
}

internal/notificationserver/handler.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,20 @@ func (h *notificationHandler) handleNotification(w http.ResponseWriter, r *http.
6161
}
6262
defer func() { _ = r.Body.Close() }()
6363

64-
var payload notificationPayload
65-
if err := json.Unmarshal(body, &payload); err != nil {
66-
log.Error(err, "parsing notification payload")
64+
if err := h.processNotification(r.Context(), body); err != nil {
65+
log.Error(err, "processing notification")
6766
http.Error(w, "bad request", http.StatusBadRequest)
6867
return
6968
}
7069

71-
ctx := r.Context()
70+
w.WriteHeader(http.StatusOK)
71+
}
72+
73+
func (h *notificationHandler) processNotification(ctx context.Context, body []byte) error {
74+
var payload notificationPayload
75+
if err := json.Unmarshal(body, &payload); err != nil {
76+
return fmt.Errorf("parsing notification payload: %w", err)
77+
}
7278

7379
var dataRecords []parsedRecord
7480
for _, rawRecord := range payload.Records {
@@ -89,7 +95,7 @@ func (h *notificationHandler) handleNotification(w http.ResponseWriter, r *http.
8995
h.dispatchDataEvents(ctx, dataRecords)
9096
}
9197

92-
w.WriteHeader(http.StatusOK)
98+
return nil
9399
}
94100

95101
func (h *notificationHandler) handleTestEvent(ctx context.Context, bucketName string) {

0 commit comments

Comments
 (0)