Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ helm install coder-logstream-kube coder-logstream-kube/coder-logstream-kube \
--set url=<your-coder-url-including-http-or-https>
```

> **Multi-Namespace support**
>
> 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:
>
> ```yaml
> # Watch specific namespaces only
> namespaces: ["default", "kube-system"]
>
> # Watch all namespaces (default)
> namespaces: []
> ```
>
> When `namespaces` is empty or not specified, the service will monitor all namespaces in the cluster.

> **Note**
> For additional customization (such as customizing the image, pull secrets, annotations, etc.), you can use the
> [values.yaml](helm/values.yaml) file directly.
Expand Down
59 changes: 47 additions & 12 deletions helm/templates/service.yaml
Original file line number Diff line number Diff line change
@@ -1,33 +1,66 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: coder-logstream-kube-role
rules:
{{- define "coder-logstream-kube.rules" -}}
- apiGroups: [""]
resources: ["pods", "events"]
verbs: ["get", "watch", "list"]
- apiGroups: ["apps"]
resources: ["replicasets", "events"]
verbs: ["get", "watch", "list"]
{{- end -}}

{{- if .Values.namespaces }}
{{- range .Values.namespaces }}
---
apiVersion: v1
kind: ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ .Values.serviceAccount.name | quote }}
annotations: {{ toYaml .Values.serviceAccount.annotations | nindent 4 }}
labels: {{ toYaml .Values.serviceAccount.labels | nindent 4 }}
name: coder-logstream-kube-role
namespace: {{ . }}
rules:
{{ include "coder-logstream-kube.rules" . | nindent 2 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: coder-logstream-kube-rolebinding
namespace: {{ . }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: coder-logstream-kube-role
subjects:
- kind: ServiceAccount
name: {{ $.Values.serviceAccount.name | quote }}
namespace: {{ $.Release.Namespace }}
{{- end }}
{{- else }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: coder-logstream-kube-role
rules:
{{ include "coder-logstream-kube.rules" . | nindent 2 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: coder-logstream-kube-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: coder-logstream-kube-role
subjects:
- kind: ServiceAccount
name: {{ .Values.serviceAccount.name | quote }}
namespace: {{ .Release.Namespace }}
{{- end }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Values.serviceAccount.name | quote }}
annotations: {{ toYaml .Values.serviceAccount.annotations | nindent 4 }}
labels: {{ toYaml .Values.serviceAccount.labels | nindent 4 }}
---
apiVersion: apps/v1
kind: Deployment
Expand Down Expand Up @@ -75,8 +108,10 @@ spec:
env:
- name: CODER_URL
value: {{ .Values.url }}
- name: CODER_NAMESPACE
value: {{ .Values.namespace | default .Release.Namespace }}
{{- if .Values.namespaces }}
- name: CODER_NAMESPACES
value: {{ join "," .Values.namespaces }}
{{- end }}
{{- if .Values.image.sslCertFile }}
- name: SSL_CERT_FILE
value: {{ .Values.image.sslCertFile }}
Expand Down
6 changes: 3 additions & 3 deletions helm/values.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# url -- The URL of your Coder deployment. Must prefix with http or https
url: ""

# namespace -- The namespace to searching for Pods within.
# If unspecified, this defaults to the Helm namespace.
namespace: ""
# namespace -- List of namespaces to search for Pods within.
# If unspecified or empty it will watch all namespaces.
namespaces: []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is breaking so make sure you inform customers in the release notes.


# volumes -- A list of extra volumes to add to the coder-logstream pod.
volumes:
Expand Down
26 changes: 19 additions & 7 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type podEventLoggerOptions struct {
logDebounce time.Duration

// The following fields are optional!
namespace string
namespaces []string
fieldSelector string
labelSelector string
}
Expand Down Expand Up @@ -78,7 +78,18 @@ func newPodEventLogger(ctx context.Context, opts podEventLoggerOptions) (*podEve
},
}

return reporter, reporter.init()
// If no namespaces are provided, we listen for events in all namespaces.
if len(opts.namespaces) == 0 {
reporter.initNamespace("")
} else {
for _, namespace := range opts.namespaces {
if err := reporter.initNamespace(namespace); err != nil {
return nil, err
}
}
}

return reporter, nil
}

type podEventLogger struct {
Expand All @@ -95,22 +106,23 @@ type podEventLogger struct {
lq *logQueuer
}

// init starts the informer factory and registers event handlers.
func (p *podEventLogger) init() error {
// initNamespace starts the informer factory and registers event handlers for a given namespace.
// If provided namespace is empty, it will start the informer factory and register event handlers for all namespaces.
func (p *podEventLogger) initNamespace(namespace string) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should include a sentence in the method comment explaining the namespace parameter

// We only track events that happen after the reporter starts.
// This is to prevent us from sending duplicate events.
startTime := time.Now()

go p.lq.work(p.ctx)

podFactory := informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(p.namespace), informers.WithTweakListOptions(func(lo *v1.ListOptions) {
podFactory := informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(namespace), informers.WithTweakListOptions(func(lo *v1.ListOptions) {
lo.FieldSelector = p.fieldSelector
lo.LabelSelector = p.labelSelector
}))
eventFactory := podFactory
if p.fieldSelector != "" || p.labelSelector != "" {
// Events cannot filter on labels and fields!
eventFactory = informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(p.namespace))
eventFactory = informers.NewSharedInformerFactoryWithOptions(p.client, 0, informers.WithNamespace(namespace))
}

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

p.logger.Info(p.ctx, "listening for pod events",
slog.F("coder_url", p.coderURL.String()),
slog.F("namespace", p.namespace),
slog.F("namespace", namespace),
slog.F("field_selector", p.fieldSelector),
slog.F("label_selector", p.labelSelector),
)
Expand Down
151 changes: 149 additions & 2 deletions logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestReplicaSetEvents(t *testing.T) {
reporter, err := newPodEventLogger(ctx, podEventLoggerOptions{
client: client,
coderURL: agentURL,
namespace: namespace,
namespaces: []string{namespace},
logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
logDebounce: 5 * time.Second,
clock: cMock,
Expand Down Expand Up @@ -144,7 +144,7 @@ func TestPodEvents(t *testing.T) {
reporter, err := newPodEventLogger(ctx, podEventLoggerOptions{
client: client,
coderURL: agentURL,
namespace: namespace,
namespaces: []string{namespace},
logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
logDebounce: 5 * time.Second,
clock: cMock,
Expand Down Expand Up @@ -221,6 +221,153 @@ func TestPodEvents(t *testing.T) {
require.NoError(t, err)
}

func Test_newPodEventLogger_multipleNamespaces(t *testing.T) {
t.Parallel()

api := newFakeAgentAPI(t)

ctx := testutil.Context(t, testutil.WaitShort)
agentURL, err := url.Parse(api.server.URL)
require.NoError(t, err)
namespaces := []string{"test-namespace1", "test-namespace2"}
client := fake.NewSimpleClientset()

cMock := quartz.NewMock(t)
reporter, err := newPodEventLogger(ctx, podEventLoggerOptions{
client: client,
coderURL: agentURL,
namespaces: namespaces,
logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
logDebounce: 5 * time.Second,
clock: cMock,
})
require.NoError(t, err)

// Create a pod in the test-namespace1 namespace
pod1 := &corev1.Pod{
ObjectMeta: v1.ObjectMeta{
Name: "test-pod-1",
Namespace: "test-namespace1",
CreationTimestamp: v1.Time{
Time: time.Now().Add(time.Hour),
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Env: []corev1.EnvVar{
{
Name: "CODER_AGENT_TOKEN",
Value: "test-token-1",
},
},
},
},
},
}
_, err = client.CoreV1().Pods("test-namespace1").Create(ctx, pod1, v1.CreateOptions{})
require.NoError(t, err)

// Create a pod in the test-namespace2 namespace
pod2 := &corev1.Pod{
ObjectMeta: v1.ObjectMeta{
Name: "test-pod-2",
Namespace: "test-namespace2",
CreationTimestamp: v1.Time{
Time: time.Now().Add(time.Hour),
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Env: []corev1.EnvVar{
{
Name: "CODER_AGENT_TOKEN",
Value: "test-token-2",
},
},
},
},
},
}
_, err = client.CoreV1().Pods("test-namespace2").Create(ctx, pod2, v1.CreateOptions{})
require.NoError(t, err)

// Wait for both pods to be registered
source1 := testutil.RequireRecvCtx(ctx, t, api.logSource)
require.Equal(t, sourceUUID, source1.ID)
require.Equal(t, "Kubernetes", source1.DisplayName)
require.Equal(t, "/icon/k8s.png", source1.Icon)

source2 := testutil.RequireRecvCtx(ctx, t, api.logSource)
require.Equal(t, sourceUUID, source2.ID)
require.Equal(t, "Kubernetes", source2.DisplayName)
require.Equal(t, "/icon/k8s.png", source2.Icon)

// Wait for both creation logs
logs1 := testutil.RequireRecvCtx(ctx, t, api.logs)
require.Len(t, logs1, 1)
require.Contains(t, logs1[0].Output, "Created pod")

logs2 := testutil.RequireRecvCtx(ctx, t, api.logs)
require.Len(t, logs2, 1)
require.Contains(t, logs2[0].Output, "Created pod")

// Create an event in the first namespace
event1 := &corev1.Event{
ObjectMeta: v1.ObjectMeta{
Name: "test-event-1",
Namespace: "test-namespace1",
CreationTimestamp: v1.Time{
Time: time.Now().Add(time.Hour),
},
},
InvolvedObject: corev1.ObjectReference{
Kind: "Pod",
Name: "test-pod-1",
Namespace: "test-namespace1",
},
Reason: "Test",
Message: "Test event for namespace1",
}
_, err = client.CoreV1().Events("test-namespace1").Create(ctx, event1, v1.CreateOptions{})
require.NoError(t, err)

// Wait for the event log
eventLogs := testutil.RequireRecvCtx(ctx, t, api.logs)
require.Len(t, eventLogs, 1)
require.Contains(t, eventLogs[0].Output, "Test event for namespace1")

// Create an event in the first namespace
event2 := &corev1.Event{
ObjectMeta: v1.ObjectMeta{
Name: "test-event-2",
Namespace: "test-namespace2",
CreationTimestamp: v1.Time{
Time: time.Now().Add(time.Hour),
},
},
InvolvedObject: corev1.ObjectReference{
Kind: "Pod",
Name: "test-pod-2",
Namespace: "test-namespace2",
},
Reason: "Test",
Message: "Test event for namespace2",
}
_, err = client.CoreV1().Events("test-namespace2").Create(ctx, event2, v1.CreateOptions{})
require.NoError(t, err)

// Wait for the event log
eventLogs2 := testutil.RequireRecvCtx(ctx, t, api.logs)
require.Len(t, eventLogs2, 1)
require.Contains(t, eventLogs2[0].Output, "Test event for namespace2")

// Clean up
err = reporter.Close()
require.NoError(t, err)
}

func Test_tokenCache(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading