Skip to content

Commit 6b0312b

Browse files
committed
feat: support watching multiple namespaces with cluster-wide RBAC
1 parent b45e9d5 commit 6b0312b

File tree

5 files changed

+187
-19
lines changed

5 files changed

+187
-19
lines changed

helm/templates/service.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
apiVersion: rbac.authorization.k8s.io/v1
2-
kind: Role
2+
kind: ClusterRole
33
metadata:
44
name: coder-logstream-kube-role
55
rules:
@@ -18,16 +18,17 @@ metadata:
1818
labels: {{ toYaml .Values.serviceAccount.labels | nindent 4 }}
1919
---
2020
apiVersion: rbac.authorization.k8s.io/v1
21-
kind: RoleBinding
21+
kind: ClusterRoleBinding
2222
metadata:
2323
name: coder-logstream-kube-rolebinding
2424
roleRef:
2525
apiGroup: rbac.authorization.k8s.io
26-
kind: Role
26+
kind: ClusterRole
2727
name: coder-logstream-kube-role
2828
subjects:
2929
- kind: ServiceAccount
3030
name: {{ .Values.serviceAccount.name | quote }}
31+
namespace: {{ .Release.Namespace }}
3132
---
3233
apiVersion: apps/v1
3334
kind: Deployment
@@ -75,8 +76,8 @@ spec:
7576
env:
7677
- name: CODER_URL
7778
value: {{ .Values.url }}
78-
- name: CODER_NAMESPACE
79-
value: {{ .Values.namespace | default .Release.Namespace }}
79+
- name: CODER_NAMESPACES
80+
value: {{ if .Values.namespaces }}{{ join "," .Values.namespaces }}{{ else }}{{ end }}
8081
{{- if .Values.image.sslCertFile }}
8182
- name: SSL_CERT_FILE
8283
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: 17 additions & 6 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 {
@@ -96,21 +107,21 @@ type podEventLogger struct {
96107
}
97108

98109
// init starts the informer factory and registers event handlers.
99-
func (p *podEventLogger) init() error {
110+
func (p *podEventLogger) initNamespace(namespace string) error {
100111
// We only track events that happen after the reporter starts.
101112
// This is to prevent us from sending duplicate events.
102113
startTime := time.Now()
103114

104115
go p.lq.work(p.ctx)
105116

106-
podFactory := informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(p.namespace), informers.WithTweakListOptions(func(lo *v1.ListOptions) {
117+
podFactory := informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(namespace), informers.WithTweakListOptions(func(lo *v1.ListOptions) {
107118
lo.FieldSelector = p.fieldSelector
108119
lo.LabelSelector = p.labelSelector
109120
}))
110121
eventFactory := podFactory
111122
if p.fieldSelector != "" || p.labelSelector != "" {
112123
// Events cannot filter on labels and fields!
113-
eventFactory = informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(p.namespace))
124+
eventFactory = informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(namespace))
114125
}
115126

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

278289
p.logger.Info(p.ctx, "listening for pod events",
279290
slog.F("coder_url", p.coderURL.String()),
280-
slog.F("namespace", p.namespace),
291+
slog.F("namespace", namespace),
281292
slog.F("field_selector", p.fieldSelector),
282293
slog.F("label_selector", p.labelSelector),
283294
)

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

main.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net/url"
77
"os"
8+
"strings"
89

910
"cdr.dev/slog"
1011
"cdr.dev/slog/sloggers/sloghuman"
@@ -27,7 +28,7 @@ func root() *cobra.Command {
2728
coderURL string
2829
fieldSelector string
2930
kubeConfig string
30-
namespace string
31+
namespacesStr string
3132
labelSelector string
3233
)
3334
cmd := &cobra.Command{
@@ -63,10 +64,18 @@ func root() *cobra.Command {
6364
return fmt.Errorf("create kubernetes client: %w", err)
6465
}
6566

67+
var namespaces []string
68+
if namespacesStr != "" {
69+
namespaces = strings.Split(namespacesStr, ",")
70+
for i, namespace := range namespaces {
71+
namespaces[i] = strings.TrimSpace(namespace)
72+
}
73+
}
74+
6675
reporter, err := newPodEventLogger(cmd.Context(), podEventLoggerOptions{
6776
coderURL: parsedURL,
6877
client: client,
69-
namespace: namespace,
78+
namespaces: namespaces,
7079
fieldSelector: fieldSelector,
7180
labelSelector: labelSelector,
7281
logger: slog.Make(sloghuman.Sink(cmd.ErrOrStderr())).Leveled(slog.LevelDebug),
@@ -85,7 +94,7 @@ func root() *cobra.Command {
8594
}
8695
cmd.Flags().StringVarP(&coderURL, "coder-url", "u", os.Getenv("CODER_URL"), "URL of the Coder instance")
8796
cmd.Flags().StringVarP(&kubeConfig, "kubeconfig", "k", "~/.kube/config", "Path to the kubeconfig file")
88-
cmd.Flags().StringVarP(&namespace, "namespace", "n", os.Getenv("CODER_NAMESPACE"), "Namespace to use when listing pods")
97+
cmd.Flags().StringVarP(&namespacesStr, "namespaces", "n", os.Getenv("CODER_NAMESPACES"), "List of namespaces to use when listing pods")
8998
cmd.Flags().StringVarP(&fieldSelector, "field-selector", "f", "", "Field selector to use when listing pods")
9099
cmd.Flags().StringVarP(&labelSelector, "label-selector", "l", "", "Label selector to use when listing pods")
91100

0 commit comments

Comments
 (0)