Skip to content

Commit bca9af2

Browse files
authored
feat: add multi-namespace support (#124)
* feat: support watching multiple namespaces with cluster-wide RBAC * Update README * Apply review suggestions
1 parent b45e9d5 commit bca9af2

File tree

6 files changed

+244
-27
lines changed

6 files changed

+244
-27
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ helm install coder-logstream-kube coder-logstream-kube/coder-logstream-kube \
2424
--set url=<your-coder-url-including-http-or-https>
2525
```
2626

27+
> **Multi-Namespace support**
28+
>
29+
> By default, coder-logstream-kube will watch all namespaces in the cluster. To limit which namespaces are monitored, you can specify them in the [values.yaml](helm/values.yaml) file:
30+
>
31+
> ```yaml
32+
> # Watch specific namespaces only
33+
> namespaces: ["default", "kube-system"]
34+
>
35+
> # Watch all namespaces (default)
36+
> namespaces: []
37+
> ```
38+
>
39+
> When `namespaces` is empty or not specified, the service will monitor all namespaces in the cluster.
40+
2741
> **Note**
2842
> For additional customization (such as customizing the image, pull secrets, annotations, etc.), you can use the
2943
> [values.yaml](helm/values.yaml) file directly.

helm/templates/service.yaml

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,66 @@
1-
apiVersion: rbac.authorization.k8s.io/v1
2-
kind: Role
3-
metadata:
4-
name: coder-logstream-kube-role
5-
rules:
1+
{{- define "coder-logstream-kube.rules" -}}
62
- apiGroups: [""]
73
resources: ["pods", "events"]
84
verbs: ["get", "watch", "list"]
95
- apiGroups: ["apps"]
106
resources: ["replicasets", "events"]
117
verbs: ["get", "watch", "list"]
8+
{{- end -}}
9+
10+
{{- if .Values.namespaces }}
11+
{{- range .Values.namespaces }}
1212
---
13-
apiVersion: v1
14-
kind: ServiceAccount
13+
apiVersion: rbac.authorization.k8s.io/v1
14+
kind: Role
1515
metadata:
16-
name: {{ .Values.serviceAccount.name | quote }}
17-
annotations: {{ toYaml .Values.serviceAccount.annotations | nindent 4 }}
18-
labels: {{ toYaml .Values.serviceAccount.labels | nindent 4 }}
16+
name: coder-logstream-kube-role
17+
namespace: {{ . }}
18+
rules:
19+
{{ include "coder-logstream-kube.rules" . | nindent 2 }}
1920
---
2021
apiVersion: rbac.authorization.k8s.io/v1
2122
kind: RoleBinding
2223
metadata:
2324
name: coder-logstream-kube-rolebinding
25+
namespace: {{ . }}
2426
roleRef:
2527
apiGroup: rbac.authorization.k8s.io
2628
kind: Role
2729
name: coder-logstream-kube-role
2830
subjects:
31+
- kind: ServiceAccount
32+
name: {{ $.Values.serviceAccount.name | quote }}
33+
namespace: {{ $.Release.Namespace }}
34+
{{- end }}
35+
{{- else }}
36+
---
37+
apiVersion: rbac.authorization.k8s.io/v1
38+
kind: ClusterRole
39+
metadata:
40+
name: coder-logstream-kube-role
41+
rules:
42+
{{ include "coder-logstream-kube.rules" . | nindent 2 }}
43+
---
44+
apiVersion: rbac.authorization.k8s.io/v1
45+
kind: ClusterRoleBinding
46+
metadata:
47+
name: coder-logstream-kube-rolebinding
48+
roleRef:
49+
apiGroup: rbac.authorization.k8s.io
50+
kind: ClusterRole
51+
name: coder-logstream-kube-role
52+
subjects:
2953
- kind: ServiceAccount
3054
name: {{ .Values.serviceAccount.name | quote }}
55+
namespace: {{ .Release.Namespace }}
56+
{{- end }}
57+
---
58+
apiVersion: v1
59+
kind: ServiceAccount
60+
metadata:
61+
name: {{ .Values.serviceAccount.name | quote }}
62+
annotations: {{ toYaml .Values.serviceAccount.annotations | nindent 4 }}
63+
labels: {{ toYaml .Values.serviceAccount.labels | nindent 4 }}
3164
---
3265
apiVersion: apps/v1
3366
kind: Deployment
@@ -75,8 +108,10 @@ spec:
75108
env:
76109
- name: CODER_URL
77110
value: {{ .Values.url }}
78-
- name: CODER_NAMESPACE
79-
value: {{ .Values.namespace | default .Release.Namespace }}
111+
{{- if .Values.namespaces }}
112+
- name: CODER_NAMESPACES
113+
value: {{ join "," .Values.namespaces }}
114+
{{- end }}
80115
{{- if .Values.image.sslCertFile }}
81116
- name: SSL_CERT_FILE
82117
value: {{ .Values.image.sslCertFile }}

helm/values.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# url -- The URL of your Coder deployment. Must prefix with http or https
22
url: ""
33

4-
# namespace -- The namespace to searching for Pods within.
5-
# If unspecified, this defaults to the Helm namespace.
6-
namespace: ""
4+
# namespace -- List of namespaces to search for Pods within.
5+
# If unspecified or empty it will watch all namespaces.
6+
namespaces: []
77

88
# volumes -- A list of extra volumes to add to the coder-logstream pod.
99
volumes:

logger.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type podEventLoggerOptions struct {
3636
logDebounce time.Duration
3737

3838
// The following fields are optional!
39-
namespace string
39+
namespaces []string
4040
fieldSelector string
4141
labelSelector string
4242
}
@@ -78,7 +78,18 @@ func newPodEventLogger(ctx context.Context, opts podEventLoggerOptions) (*podEve
7878
},
7979
}
8080

81-
return reporter, reporter.init()
81+
// If no namespaces are provided, we listen for events in all namespaces.
82+
if len(opts.namespaces) == 0 {
83+
reporter.initNamespace("")
84+
} else {
85+
for _, namespace := range opts.namespaces {
86+
if err := reporter.initNamespace(namespace); err != nil {
87+
return nil, err
88+
}
89+
}
90+
}
91+
92+
return reporter, nil
8293
}
8394

8495
type podEventLogger struct {
@@ -95,22 +106,23 @@ type podEventLogger struct {
95106
lq *logQueuer
96107
}
97108

98-
// init starts the informer factory and registers event handlers.
99-
func (p *podEventLogger) init() error {
109+
// initNamespace starts the informer factory and registers event handlers for a given namespace.
110+
// If provided namespace is empty, it will start the informer factory and register event handlers for all namespaces.
111+
func (p *podEventLogger) initNamespace(namespace string) error {
100112
// We only track events that happen after the reporter starts.
101113
// This is to prevent us from sending duplicate events.
102114
startTime := time.Now()
103115

104116
go p.lq.work(p.ctx)
105117

106-
podFactory := informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(p.namespace), informers.WithTweakListOptions(func(lo *v1.ListOptions) {
118+
podFactory := informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(namespace), informers.WithTweakListOptions(func(lo *v1.ListOptions) {
107119
lo.FieldSelector = p.fieldSelector
108120
lo.LabelSelector = p.labelSelector
109121
}))
110122
eventFactory := podFactory
111123
if p.fieldSelector != "" || p.labelSelector != "" {
112124
// Events cannot filter on labels and fields!
113-
eventFactory = informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(p.namespace))
125+
eventFactory = informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(namespace))
114126
}
115127

116128
// We listen for Pods and Events in the informer factory.
@@ -277,7 +289,7 @@ func (p *podEventLogger) init() error {
277289

278290
p.logger.Info(p.ctx, "listening for pod events",
279291
slog.F("coder_url", p.coderURL.String()),
280-
slog.F("namespace", p.namespace),
292+
slog.F("namespace", namespace),
281293
slog.F("field_selector", p.fieldSelector),
282294
slog.F("label_selector", p.labelSelector),
283295
)

logger_test.go

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func TestReplicaSetEvents(t *testing.T) {
4747
reporter, err := newPodEventLogger(ctx, podEventLoggerOptions{
4848
client: client,
4949
coderURL: agentURL,
50-
namespace: namespace,
50+
namespaces: []string{namespace},
5151
logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
5252
logDebounce: 5 * time.Second,
5353
clock: cMock,
@@ -144,7 +144,7 @@ func TestPodEvents(t *testing.T) {
144144
reporter, err := newPodEventLogger(ctx, podEventLoggerOptions{
145145
client: client,
146146
coderURL: agentURL,
147-
namespace: namespace,
147+
namespaces: []string{namespace},
148148
logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
149149
logDebounce: 5 * time.Second,
150150
clock: cMock,
@@ -221,6 +221,153 @@ func TestPodEvents(t *testing.T) {
221221
require.NoError(t, err)
222222
}
223223

224+
func Test_newPodEventLogger_multipleNamespaces(t *testing.T) {
225+
t.Parallel()
226+
227+
api := newFakeAgentAPI(t)
228+
229+
ctx := testutil.Context(t, testutil.WaitShort)
230+
agentURL, err := url.Parse(api.server.URL)
231+
require.NoError(t, err)
232+
namespaces := []string{"test-namespace1", "test-namespace2"}
233+
client := fake.NewSimpleClientset()
234+
235+
cMock := quartz.NewMock(t)
236+
reporter, err := newPodEventLogger(ctx, podEventLoggerOptions{
237+
client: client,
238+
coderURL: agentURL,
239+
namespaces: namespaces,
240+
logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
241+
logDebounce: 5 * time.Second,
242+
clock: cMock,
243+
})
244+
require.NoError(t, err)
245+
246+
// Create a pod in the test-namespace1 namespace
247+
pod1 := &corev1.Pod{
248+
ObjectMeta: v1.ObjectMeta{
249+
Name: "test-pod-1",
250+
Namespace: "test-namespace1",
251+
CreationTimestamp: v1.Time{
252+
Time: time.Now().Add(time.Hour),
253+
},
254+
},
255+
Spec: corev1.PodSpec{
256+
Containers: []corev1.Container{
257+
{
258+
Env: []corev1.EnvVar{
259+
{
260+
Name: "CODER_AGENT_TOKEN",
261+
Value: "test-token-1",
262+
},
263+
},
264+
},
265+
},
266+
},
267+
}
268+
_, err = client.CoreV1().Pods("test-namespace1").Create(ctx, pod1, v1.CreateOptions{})
269+
require.NoError(t, err)
270+
271+
// Create a pod in the test-namespace2 namespace
272+
pod2 := &corev1.Pod{
273+
ObjectMeta: v1.ObjectMeta{
274+
Name: "test-pod-2",
275+
Namespace: "test-namespace2",
276+
CreationTimestamp: v1.Time{
277+
Time: time.Now().Add(time.Hour),
278+
},
279+
},
280+
Spec: corev1.PodSpec{
281+
Containers: []corev1.Container{
282+
{
283+
Env: []corev1.EnvVar{
284+
{
285+
Name: "CODER_AGENT_TOKEN",
286+
Value: "test-token-2",
287+
},
288+
},
289+
},
290+
},
291+
},
292+
}
293+
_, err = client.CoreV1().Pods("test-namespace2").Create(ctx, pod2, v1.CreateOptions{})
294+
require.NoError(t, err)
295+
296+
// Wait for both pods to be registered
297+
source1 := testutil.RequireRecvCtx(ctx, t, api.logSource)
298+
require.Equal(t, sourceUUID, source1.ID)
299+
require.Equal(t, "Kubernetes", source1.DisplayName)
300+
require.Equal(t, "/icon/k8s.png", source1.Icon)
301+
302+
source2 := testutil.RequireRecvCtx(ctx, t, api.logSource)
303+
require.Equal(t, sourceUUID, source2.ID)
304+
require.Equal(t, "Kubernetes", source2.DisplayName)
305+
require.Equal(t, "/icon/k8s.png", source2.Icon)
306+
307+
// Wait for both creation logs
308+
logs1 := testutil.RequireRecvCtx(ctx, t, api.logs)
309+
require.Len(t, logs1, 1)
310+
require.Contains(t, logs1[0].Output, "Created pod")
311+
312+
logs2 := testutil.RequireRecvCtx(ctx, t, api.logs)
313+
require.Len(t, logs2, 1)
314+
require.Contains(t, logs2[0].Output, "Created pod")
315+
316+
// Create an event in the first namespace
317+
event1 := &corev1.Event{
318+
ObjectMeta: v1.ObjectMeta{
319+
Name: "test-event-1",
320+
Namespace: "test-namespace1",
321+
CreationTimestamp: v1.Time{
322+
Time: time.Now().Add(time.Hour),
323+
},
324+
},
325+
InvolvedObject: corev1.ObjectReference{
326+
Kind: "Pod",
327+
Name: "test-pod-1",
328+
Namespace: "test-namespace1",
329+
},
330+
Reason: "Test",
331+
Message: "Test event for namespace1",
332+
}
333+
_, err = client.CoreV1().Events("test-namespace1").Create(ctx, event1, v1.CreateOptions{})
334+
require.NoError(t, err)
335+
336+
// Wait for the event log
337+
eventLogs := testutil.RequireRecvCtx(ctx, t, api.logs)
338+
require.Len(t, eventLogs, 1)
339+
require.Contains(t, eventLogs[0].Output, "Test event for namespace1")
340+
341+
// Create an event in the first namespace
342+
event2 := &corev1.Event{
343+
ObjectMeta: v1.ObjectMeta{
344+
Name: "test-event-2",
345+
Namespace: "test-namespace2",
346+
CreationTimestamp: v1.Time{
347+
Time: time.Now().Add(time.Hour),
348+
},
349+
},
350+
InvolvedObject: corev1.ObjectReference{
351+
Kind: "Pod",
352+
Name: "test-pod-2",
353+
Namespace: "test-namespace2",
354+
},
355+
Reason: "Test",
356+
Message: "Test event for namespace2",
357+
}
358+
_, err = client.CoreV1().Events("test-namespace2").Create(ctx, event2, v1.CreateOptions{})
359+
require.NoError(t, err)
360+
361+
// Wait for the event log
362+
eventLogs2 := testutil.RequireRecvCtx(ctx, t, api.logs)
363+
require.Len(t, eventLogs2, 1)
364+
require.Contains(t, eventLogs2[0].Output, "Test event for namespace2")
365+
366+
// Clean up
367+
err = reporter.Close()
368+
require.NoError(t, err)
369+
}
370+
224371
func Test_tokenCache(t *testing.T) {
225372
t.Parallel()
226373

0 commit comments

Comments
 (0)