Skip to content

Commit 9e8e021

Browse files
authored
Merge pull request #521 from neo4j/add-pod-owner-reference
Fix Kyverno require-valid-owner-reference for Neo4j operations pods
2 parents eb48ff7 + 950a2bd commit 9e8e021

File tree

4 files changed

+136
-99
lines changed

4 files changed

+136
-99
lines changed

internal/unit_tests/helm_template_neo4j_operations_test.go

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/neo4j/helm-charts/internal/model"
88
"github.com/stretchr/testify/assert"
9+
batchv1 "k8s.io/api/batch/v1"
910
v1 "k8s.io/api/core/v1"
1011
v12 "k8s.io/api/rbac/v1"
1112
)
@@ -32,15 +33,16 @@ func TestNeo4jOperationsEnableServer(t *testing.T) {
3233
return
3334
}
3435

35-
operationsPod := manifest.OfTypeWithName(
36-
&v1.Pod{},
36+
operationsJob := manifest.OfTypeWithName(
37+
&batchv1.Job{},
3738
fmt.Sprintf("%s-operations", model.DefaultHelmTemplateReleaseName.String()),
38-
).(*v1.Pod)
39-
assert.NotNil(t, operationsPod, "operations pod not found")
40-
assert.Equal(t, operationsPod.Spec.RestartPolicy, v1.RestartPolicyNever)
41-
assert.Len(t, operationsPod.Spec.Containers, 1)
39+
).(*batchv1.Job)
40+
assert.NotNil(t, operationsJob, "operations job not found")
41+
podSpec := operationsJob.Spec.Template.Spec
42+
assert.Equal(t, podSpec.RestartPolicy, v1.RestartPolicyNever)
43+
assert.Len(t, podSpec.Containers, 1)
4244
envVarNames := make(map[string]bool)
43-
for _, envVar := range operationsPod.Spec.Containers[0].Env {
45+
for _, envVar := range podSpec.Containers[0].Env {
4446
envVarNames[envVar.Name] = true
4547
}
4648

@@ -50,7 +52,7 @@ func TestNeo4jOperationsEnableServer(t *testing.T) {
5052
assert.True(t, envVarNames[required], "Required environment variable %s not found", required)
5153
}
5254

53-
for _, envVar := range operationsPod.Spec.Containers[0].Env {
55+
for _, envVar := range podSpec.Containers[0].Env {
5456
switch envVar.Name {
5557
case "RELEASE_NAME", "NAMESPACE", "SECRETNAME", "PROTOCOL":
5658
case "SSL_DISABLE_HOSTNAME_VERIFICATION", "SSL_INSECURE_SKIP_VERIFY":
@@ -59,7 +61,7 @@ func TestNeo4jOperationsEnableServer(t *testing.T) {
5961
}
6062
}
6163

62-
for _, envVar := range operationsPod.Spec.Containers[0].Env {
64+
for _, envVar := range podSpec.Containers[0].Env {
6365
switch envVar.Name {
6466
case "RELEASE_NAME":
6567
assert.Equal(t, envVar.Value, model.DefaultHelmTemplateReleaseName.String())
@@ -71,32 +73,33 @@ func TestNeo4jOperationsEnableServer(t *testing.T) {
7173
assert.Equal(t, envVar.Value, "neo4j")
7274
}
7375
}
74-
assert.Contains(t, operationsPod.ObjectMeta.Labels, "testkey")
76+
assert.Contains(t, operationsJob.ObjectMeta.Labels, "testkey")
7577

7678
operationsRole := manifest.OfTypeWithName(
7779
&v12.Role{},
78-
fmt.Sprintf("%s-secrets-reader", model.DefaultHelmTemplateReleaseName.String()),
80+
fmt.Sprintf("%s-operations", model.DefaultHelmTemplateReleaseName.String()),
7981
).(*v12.Role)
8082
assert.NotNil(t, operationsRole, "operations role not found")
8183
assert.Len(t, operationsRole.Rules, 1)
82-
assert.Equal(t, operationsRole.Rules[0].Verbs, []string{"get", "watch", "list"})
84+
assert.Equal(t, operationsRole.Rules[0].Verbs, []string{"get"})
8385
assert.Equal(t, operationsRole.Rules[0].Resources, []string{"secrets"})
86+
assert.NotEmpty(t, operationsRole.Rules[0].ResourceNames, "operations role should have resourceNames for least-privilege")
8487

85-
serviceAccount := manifest.OfTypeWithName(
88+
operationsServiceAccount := manifest.OfTypeWithName(
8689
&v1.ServiceAccount{},
87-
model.DefaultHelmTemplateReleaseName.String(),
90+
fmt.Sprintf("%s-operations", model.DefaultHelmTemplateReleaseName.String()),
8891
).(*v1.ServiceAccount)
89-
assert.NotNil(t, serviceAccount, "serviceaccount not found")
92+
assert.NotNil(t, operationsServiceAccount, "operations serviceaccount not found")
9093

9194
operationsRoleBinding := manifest.OfTypeWithName(
9295
&v12.RoleBinding{},
93-
fmt.Sprintf("%s-secrets-binding", model.DefaultHelmTemplateReleaseName.String()),
96+
fmt.Sprintf("%s-operations", model.DefaultHelmTemplateReleaseName.String()),
9497
).(*v12.RoleBinding)
9598
assert.NotNil(t, operationsRoleBinding, "operations role binding not found")
9699
assert.Equal(t, operationsRoleBinding.RoleRef.Name, operationsRole.Name)
97100
assert.Len(t, operationsRoleBinding.Subjects, 1)
98101
assert.Equal(t, operationsRoleBinding.Subjects[0].Kind, "ServiceAccount")
99-
assert.Equal(t, operationsRoleBinding.Subjects[0].Name, serviceAccount.Name)
102+
assert.Equal(t, operationsRoleBinding.Subjects[0].Name, operationsServiceAccount.Name)
100103

101104
}
102105

@@ -125,15 +128,15 @@ func TestNeo4jOperationsWithSSLConfiguration(t *testing.T) {
125128
return
126129
}
127130

128-
operationsPod := manifest.OfTypeWithName(
129-
&v1.Pod{},
131+
operationsJob := manifest.OfTypeWithName(
132+
&batchv1.Job{},
130133
fmt.Sprintf("%s-operations", model.DefaultHelmTemplateReleaseName.String()),
131-
).(*v1.Pod)
132-
assert.NotNil(t, operationsPod, "operations pod not found")
134+
).(*batchv1.Job)
135+
assert.NotNil(t, operationsJob, "operations job not found")
133136

134137
// Check for SSL environment variables
135138
envVars := make(map[string]string)
136-
for _, envVar := range operationsPod.Spec.Containers[0].Env {
139+
for _, envVar := range operationsJob.Spec.Template.Spec.Containers[0].Env {
137140
envVars[envVar.Name] = envVar.Value
138141
}
139142

@@ -159,15 +162,15 @@ func TestNeo4jOperationsEnableServerForStandalone(t *testing.T) {
159162
return
160163
}
161164

162-
operationsPod := manifest.OfTypeWithName(
163-
&v1.Pod{},
165+
operationsJob := manifest.OfTypeWithName(
166+
&batchv1.Job{},
164167
fmt.Sprintf("%s-operations", model.DefaultHelmTemplateReleaseName.String()),
165168
)
166-
assert.Nil(t, operationsPod, "operations pod should not be present for standalone")
169+
assert.Nil(t, operationsJob, "operations job should not be present for standalone")
167170

168171
operationsRole := manifest.OfTypeWithName(
169172
&v12.Role{},
170-
fmt.Sprintf("%s-secrets-reader", model.DefaultHelmTemplateReleaseName.String()),
173+
fmt.Sprintf("%s-operations", model.DefaultHelmTemplateReleaseName.String()),
171174
)
172175
assert.Nil(t, operationsRole, "operations role should not be present for standalone")
173176

@@ -194,13 +197,13 @@ func TestNeo4jOperationsImagePullSecrets(t *testing.T) {
194197
return
195198
}
196199

197-
operationsPod := manifest.OfTypeWithName(
198-
&v1.Pod{},
200+
operationsJob := manifest.OfTypeWithName(
201+
&batchv1.Job{},
199202
fmt.Sprintf("%s-operations", model.DefaultHelmTemplateReleaseName.String()),
200-
).(*v1.Pod)
201-
assert.NotNil(t, operationsPod, "operations pod not found")
203+
).(*batchv1.Job)
204+
assert.NotNil(t, operationsJob, "operations job not found")
202205

203-
pullSecrets := operationsPod.Spec.ImagePullSecrets
206+
pullSecrets := operationsJob.Spec.Template.Spec.ImagePullSecrets
204207
assert.Len(t, pullSecrets, 2, "should have 2 imagePullSecrets")
205208
assert.Equal(t, "my-pull-secret", pullSecrets[0].Name)
206209
assert.Equal(t, "another-secret", pullSecrets[1].Name)
@@ -227,12 +230,12 @@ func TestNeo4jOperationsImagePullSecretsEmpty(t *testing.T) {
227230
return
228231
}
229232

230-
operationsPod := manifest.OfTypeWithName(
231-
&v1.Pod{},
233+
operationsJob := manifest.OfTypeWithName(
234+
&batchv1.Job{},
232235
fmt.Sprintf("%s-operations", model.DefaultHelmTemplateReleaseName.String()),
233-
).(*v1.Pod)
234-
assert.NotNil(t, operationsPod, "operations pod not found")
236+
).(*batchv1.Job)
237+
assert.NotNil(t, operationsJob, "operations job not found")
235238

236-
pullSecrets := operationsPod.Spec.ImagePullSecrets
239+
pullSecrets := operationsJob.Spec.Template.Spec.ImagePullSecrets
237240
assert.Nil(t, pullSecrets, "imagePullSecrets should be nil when empty")
238241
}
Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,99 @@
11
{{- $clusterEnabled := eq (include "neo4j.isClusterEnabled" .) "true" }}
22

33
{{- if and (not (kindIs "invalid" $.Values.neo4j.operations)) $.Values.neo4j.operations.enableServer $clusterEnabled -}}
4-
apiVersion: v1
5-
kind: Pod
4+
# Job is used instead of a standalone Pod so that the created Pod has ownerReferences.
5+
# This satisfies Kyverno policies (e.g. require-valid-owner-reference) that require pods
6+
# to be owned by Deployment, ReplicaSet, Job, StatefulSet, DaemonSet, etc.
7+
# Uses a dedicated ServiceAccount with least-privilege RBAC (get on auth secret only).
8+
apiVersion: batch/v1
9+
kind: Job
610
metadata:
711
name: {{ include "neo4j.fullname" . }}-operations
812
labels:
913
app: "neo4j-operations"
1014
{{- include "neo4j.labels" $.Values.neo4j.operations | indent 4 }}
1115
spec:
12-
restartPolicy: Never
13-
serviceAccountName: {{ include "neo4j.serviceAccountName" . }}
14-
{{- include "neo4j.imagePullSecrets" .Values.image.imagePullSecrets | indent 2 }}
15-
containers:
16-
- name: {{ include "neo4j.fullname" . }}-operations
17-
image: {{ $.Values.neo4j.operations.image }}
18-
imagePullPolicy: "Always"
19-
env:
20-
- name: RELEASE_NAME
21-
value: {{ include "neo4j.fullname" . | quote }}
22-
{{- if and (not (kindIs "invalid" $.Values.neo4j.passwordFromSecret)) (not (empty $.Values.neo4j.passwordFromSecret)) }}
23-
- name: SECRETNAME
24-
value: {{ $.Values.neo4j.passwordFromSecret | quote }}
25-
{{- else }}
26-
- name: SECRETNAME
27-
value: {{ include "neo4j.name" . | printf "%s-auth" | quote }}
28-
{{- end }}
29-
- name: NAMESPACE
30-
value: {{ .Release.Namespace | quote }}
31-
- name: PROTOCOL
32-
value: {{ $.Values.neo4j.operations.protocol | default "neo4j" | quote }}
33-
{{- if $.Values.neo4j.operations.ssl }}
34-
- name: SSL_DISABLE_HOSTNAME_VERIFICATION
35-
value: {{ $.Values.neo4j.operations.ssl.disableHostnameVerification | default false | toString | quote }}
36-
- name: SSL_INSECURE_SKIP_VERIFY
37-
value: {{ $.Values.neo4j.operations.ssl.insecureSkipVerify | default false | toString | quote }}
38-
{{- end }}
16+
backoffLimit: 0
17+
template:
18+
metadata:
19+
labels:
20+
app: "neo4j-operations"
21+
{{- include "neo4j.labels" $.Values.neo4j.operations | indent 8 }}
22+
spec:
23+
restartPolicy: Never
24+
serviceAccountName: {{ include "neo4j.fullname" . }}-operations
25+
{{- with .Values.securityContext }}
26+
securityContext: {{ toYaml . | nindent 8 }}
27+
{{- end }}
28+
{{- include "neo4j.imagePullSecrets" .Values.image.imagePullSecrets | indent 6 }}
29+
containers:
30+
- name: {{ include "neo4j.fullname" . }}-operations
31+
image: {{ $.Values.neo4j.operations.image }}
32+
imagePullPolicy: {{ $.Values.neo4j.operations.imagePullPolicy | default "Always" }}
33+
{{- with $.Values.containerSecurityContext }}
34+
securityContext: {{ toYaml . | nindent 12 }}
35+
{{- end }}
36+
{{- with $.Values.neo4j.operations.resources }}
37+
resources: {{ toYaml . | nindent 12 }}
38+
{{- end }}
39+
env:
40+
- name: RELEASE_NAME
41+
value: {{ include "neo4j.fullname" . | quote }}
42+
{{- if and (not (kindIs "invalid" $.Values.neo4j.passwordFromSecret)) (not (empty $.Values.neo4j.passwordFromSecret)) }}
43+
- name: SECRETNAME
44+
value: {{ $.Values.neo4j.passwordFromSecret | quote }}
45+
{{- else }}
46+
- name: SECRETNAME
47+
value: {{ include "neo4j.name" . | printf "%s-auth" | quote }}
48+
{{- end }}
49+
- name: NAMESPACE
50+
value: {{ .Release.Namespace | quote }}
51+
- name: PROTOCOL
52+
value: {{ $.Values.neo4j.operations.protocol | default "neo4j" | quote }}
53+
{{- if $.Values.neo4j.operations.ssl }}
54+
- name: SSL_DISABLE_HOSTNAME_VERIFICATION
55+
value: {{ $.Values.neo4j.operations.ssl.disableHostnameVerification | default false | toString | quote }}
56+
- name: SSL_INSECURE_SKIP_VERIFY
57+
value: {{ $.Values.neo4j.operations.ssl.insecureSkipVerify | default false | toString | quote }}
58+
{{- end }}
59+
---
60+
apiVersion: v1
61+
kind: ServiceAccount
62+
metadata:
63+
namespace: {{ .Release.Namespace | quote }}
64+
name: {{ include "neo4j.fullname" . }}-operations
65+
labels:
66+
app: "neo4j-operations"
67+
{{- include "neo4j.labels" $.Values.neo4j.operations | indent 4 }}
68+
---
69+
apiVersion: rbac.authorization.k8s.io/v1
70+
kind: Role
71+
metadata:
72+
namespace: {{ .Release.Namespace | quote }}
73+
name: {{ include "neo4j.fullname" . }}-operations
74+
labels:
75+
app: "neo4j-operations"
76+
{{- include "neo4j.labels" $.Values.neo4j.operations | indent 4 }}
77+
rules:
78+
- apiGroups: [""]
79+
resources: ["secrets"]
80+
resourceNames: [{{ include "neo4j.secretName" . | quote }}]
81+
verbs: ["get"]
82+
---
83+
apiVersion: rbac.authorization.k8s.io/v1
84+
kind: RoleBinding
85+
metadata:
86+
namespace: {{ .Release.Namespace | quote }}
87+
name: {{ include "neo4j.fullname" . }}-operations
88+
labels:
89+
app: "neo4j-operations"
90+
{{- include "neo4j.labels" $.Values.neo4j.operations | indent 4 }}
91+
subjects:
92+
- kind: ServiceAccount
93+
name: {{ include "neo4j.fullname" . }}-operations
94+
namespace: {{ .Release.Namespace }}
95+
roleRef:
96+
kind: Role
97+
name: {{ include "neo4j.fullname" . }}-operations
98+
apiGroup: rbac.authorization.k8s.io
3999
{{- end -}}

neo4j/templates/neo4j-service-account.yaml

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -38,36 +38,4 @@ roleRef:
3838
kind: Role # this must be Role or ClusterRole
3939
name: {{ include "neo4j.fullname" . }}-service-reader # this must match the name of the Role or ClusterRole you wish to bind to
4040
apiGroup: rbac.authorization.k8s.io
41-
---
42-
{{- if and (not (kindIs "invalid" $.Values.neo4j.operations)) $.Values.neo4j.operations.enableServer }}
43-
apiVersion: rbac.authorization.k8s.io/v1
44-
kind: Role
45-
metadata:
46-
namespace: "{{ .Release.Namespace }}"
47-
name: "{{ include "neo4j.fullname" . }}-secrets-reader"
48-
labels:
49-
app: "{{ template "neo4j.name" $ }}"
50-
{{- include "neo4j.labels" $.Values.neo4j | indent 4 }}
51-
rules:
52-
- apiGroups: [""] # "" indicates the core API group
53-
resources: ["secrets"]
54-
verbs: ["get", "watch", "list"]
55-
---
56-
apiVersion: rbac.authorization.k8s.io/v1
57-
kind: RoleBinding
58-
metadata:
59-
namespace: "{{ .Release.Namespace }}"
60-
name: "{{ include "neo4j.fullname" . }}-secrets-binding"
61-
labels:
62-
app: "{{ template "neo4j.name" $ }}"
63-
{{- include "neo4j.labels" $.Values.neo4j | indent 4 }}
64-
subjects:
65-
- kind: ServiceAccount
66-
name: {{ include "neo4j.serviceAccountName" . }}
67-
roleRef:
68-
# "roleRef" specifies the binding to a Role / ClusterRole
69-
kind: Role # this must be Role or ClusterRole
70-
name: {{ include "neo4j.fullname" . }}-secrets-reader # this must match the name of the Role or ClusterRole you wish to bind to
71-
apiGroup: rbac.authorization.k8s.io
7241
{{- end }}
73-
{{- end }}

neo4j/values.yaml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,24 @@ neo4j:
3030

3131
# (Clustering only feature)
3232
# Neo4j operations allows you to enable servers (part of cluster) which are added outside the minimumClusterSize
33-
# When the enableServer flag is set to true , an operations pod is created which performs the following functions
33+
# When the enableServer flag is set to true, an operations Job (creating a Pod with ownerReferences) is created which performs the following functions
3434
# fetch neo4j creds from the k8s secret (provided by user or created via helm chart)
3535
# Use the cluster ip created as part of the respective release to connect to Neo4j via Go Driver
3636
# Execute the ENABLE SERVER query and enable the server
37-
# The operations pod ends successfully if the server is enabled, or it was already enabled
37+
# The operations Job's Pod ends successfully if the server is enabled, or it was already enabled
3838
operations:
3939
enableServer: false
4040
image: "neo4j/helm-charts-operations:2026.01.4"
41+
# imagePullPolicy: Always (default) or IfNotPresent
42+
imagePullPolicy: "Always"
4143
# protocol can be "neo4j or "neo4j+ssc" or "neo4j+s". Default set to neo4j
4244
# Note: Do not specify bolt protocol here...it will FAIL.
4345
protocol: "neo4j"
4446
labels: {}
47+
# Resource requests and limits for the operations container
48+
resources:
49+
cpu: "100m"
50+
memory: "128Mi"
4551
# SSL/TLS configuration for operations pod connections
4652
ssl:
4753
# Set to true to disable hostname verification for TLS connections

0 commit comments

Comments
 (0)