diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 3fa5655cbc..343c643b21 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -33,3 +33,12 @@ jobs:
build:
uses: ./.github/workflows/build.yml
+
+ helm_unit_tests:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - name: Set up Helm
+ uses: azure/setup-helm@v4
+ - name: Run Helm unit tests
+ run: ./helm/run-tests.sh
diff --git a/helm/generic-helm-chart/Chart.yaml b/helm/generic-helm-chart/Chart.yaml
new file mode 100644
index 0000000000..9deb8046bd
--- /dev/null
+++ b/helm/generic-helm-chart/Chart.yaml
@@ -0,0 +1,22 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v2
+name: generic-operator-chart
+description: A generic reusable Helm chart for Java operators built with Java Operator SDK (JOSDK)
+type: application
+version: 0.1.0
+appVersion: "0.1.0"
diff --git a/helm/generic-helm-chart/templates/_helpers.tpl b/helm/generic-helm-chart/templates/_helpers.tpl
new file mode 100644
index 0000000000..337cf741b8
--- /dev/null
+++ b/helm/generic-helm-chart/templates/_helpers.tpl
@@ -0,0 +1,100 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "generic-operator.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+*/}}
+{{- define "generic-operator.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "generic-operator.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "generic-operator.labels" -}}
+helm.sh/chart: {{ include "generic-operator.chart" . }}
+{{ include "generic-operator.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "generic-operator.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "generic-operator.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "generic-operator.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "generic-operator.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
+
+{{/*
+Image tag - defaults to chart appVersion
+*/}}
+{{- define "generic-operator.imageTag" -}}
+{{- .Values.image.tag | default .Chart.AppVersion }}
+{{- end }}
+
+{{/*
+Default verbs for primary resources
+*/}}
+{{- define "generic-operator.primaryVerbs" -}}
+- get
+- list
+- watch
+- patch
+- update
+{{- end }}
+
+{{/*
+Default verbs for primary resource status
+*/}}
+{{- define "generic-operator.primaryStatusVerbs" -}}
+- get
+- patch
+- update
+{{- end }}
+
+{{/*
+Default verbs for secondary resources
+*/}}
+{{- define "generic-operator.secondaryVerbs" -}}
+- get
+- list
+- watch
+- create
+- update
+- patch
+- delete
+{{- end }}
diff --git a/helm/generic-helm-chart/templates/clusterrole.yaml b/helm/generic-helm-chart/templates/clusterrole.yaml
new file mode 100644
index 0000000000..04070da489
--- /dev/null
+++ b/helm/generic-helm-chart/templates/clusterrole.yaml
@@ -0,0 +1,97 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+{{- if .Values.rbac.create }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "generic-operator.fullname" . }}
+ labels:
+ {{- include "generic-operator.labels" . | nindent 4 }}
+rules:
+{{- /* Primary resources - the custom resources the operator reconciles */}}
+{{- range .Values.primaryResources }}
+- apiGroups:
+ - {{ .apiGroup | quote }}
+ resources:
+ {{- range .resources }}
+ - {{ . }}
+ {{- end }}
+ verbs:
+ {{- if .verbs }}
+ {{- range .verbs }}
+ - {{ . }}
+ {{- end }}
+ {{- else }}
+ {{- include "generic-operator.primaryVerbs" $ | nindent 2 }}
+ {{- end }}
+- apiGroups:
+ - {{ .apiGroup | quote }}
+ resources:
+ {{- range .resources }}
+ - {{ . }}/status
+ {{- end }}
+ verbs:
+ {{- if .statusVerbs }}
+ {{- range .statusVerbs }}
+ - {{ . }}
+ {{- end }}
+ {{- else }}
+ {{- include "generic-operator.primaryStatusVerbs" $ | nindent 2 }}
+ {{- end }}
+{{- end }}
+{{- /* Secondary resources - resources managed by the operator */}}
+{{- range .Values.secondaryResources }}
+- apiGroups:
+ - {{ .apiGroup | quote }}
+ resources:
+ {{- range .resources }}
+ - {{ . }}
+ {{- end }}
+ verbs:
+ {{- if .verbs }}
+ {{- range .verbs }}
+ - {{ . }}
+ {{- end }}
+ {{- else }}
+ {{- include "generic-operator.secondaryVerbs" $ | nindent 2 }}
+ {{- end }}
+{{- end }}
+# Event permissions - for recording events
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
+{{- /* Leader election - Lease permissions */}}
+{{- if .Values.leaderElection.enabled }}
+# Lease permissions - for leader election
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+{{- end }}
+{{- end }}
diff --git a/helm/generic-helm-chart/templates/clusterrolebinding.yaml b/helm/generic-helm-chart/templates/clusterrolebinding.yaml
new file mode 100644
index 0000000000..d96ddaaab1
--- /dev/null
+++ b/helm/generic-helm-chart/templates/clusterrolebinding.yaml
@@ -0,0 +1,32 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+{{- if .Values.rbac.create }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "generic-operator.fullname" . }}
+ labels:
+ {{- include "generic-operator.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: {{ include "generic-operator.fullname" . }}
+subjects:
+- kind: ServiceAccount
+ name: {{ include "generic-operator.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace }}
+{{- end }}
diff --git a/helm/generic-helm-chart/templates/configmap.yaml b/helm/generic-helm-chart/templates/configmap.yaml
new file mode 100644
index 0000000000..3fb9b5b6af
--- /dev/null
+++ b/helm/generic-helm-chart/templates/configmap.yaml
@@ -0,0 +1,27 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "generic-operator.fullname" . }}-config
+ labels:
+ {{- include "generic-operator.labels" . | nindent 4 }}
+data:
+ config.yaml: |
+ {{- index .Values.operatorConfig "config" | nindent 4 }}
+ log4j2.xml: |
+ {{- .Values.operatorConfig.log4j2 | nindent 4 }}
diff --git a/helm/generic-helm-chart/templates/deployment.yaml b/helm/generic-helm-chart/templates/deployment.yaml
new file mode 100644
index 0000000000..dd06916155
--- /dev/null
+++ b/helm/generic-helm-chart/templates/deployment.yaml
@@ -0,0 +1,87 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "generic-operator.fullname" . }}
+ labels:
+ {{- include "generic-operator.labels" . | nindent 4 }}
+spec:
+ replicas: {{ .Values.replicaCount }}
+ selector:
+ matchLabels:
+ {{- include "generic-operator.selectorLabels" . | nindent 6 }}
+ template:
+ metadata:
+ annotations:
+ checksum/config: {{ .Values.operatorConfig | toJson | sha256sum }}
+ {{- with .Values.podAnnotations }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ labels:
+ {{- include "generic-operator.selectorLabels" . | nindent 8 }}
+ spec:
+ {{- with .Values.podSecurityContext }}
+ securityContext:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.imagePullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ serviceAccountName: {{ include "generic-operator.serviceAccountName" . }}
+ {{- with .Values.extraInitContainers }}
+ initContainers:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ containers:
+ - name: operator
+ securityContext:
+ {{- toYaml .Values.securityContext | nindent 12 }}
+ image: "{{ required "A valid .Values.image.repository is required" .Values.image.repository }}:{{ include "generic-operator.imageTag" . }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ env:
+ - name: OPERATOR_NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ {{- if .Values.operator.watchNamespace }}
+ - name: WATCH_NAMESPACE
+ value: {{ .Values.operator.watchNamespace | quote }}
+ {{- end }}
+ {{- with .Values.operator.env }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ resources:
+ {{- toYaml .Values.resources | nindent 12 }}
+ volumeMounts:
+ - name: config
+ mountPath: /config
+ readOnly: true
+ {{- with .Values.extraVolumeMounts }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.extraContainers }}
+ {{- toYaml . | nindent 6 }}
+ {{- end }}
+ volumes:
+ - name: config
+ configMap:
+ name: {{ include "generic-operator.fullname" . }}-config
+ {{- with .Values.extraVolumes }}
+ {{- toYaml . | nindent 6 }}
+ {{- end }}
diff --git a/helm/generic-helm-chart/templates/serviceaccount.yaml b/helm/generic-helm-chart/templates/serviceaccount.yaml
new file mode 100644
index 0000000000..7ddc5e39c1
--- /dev/null
+++ b/helm/generic-helm-chart/templates/serviceaccount.yaml
@@ -0,0 +1,28 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+{{- if .Values.serviceAccount.create }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ include "generic-operator.serviceAccountName" . }}
+ labels:
+ {{- include "generic-operator.labels" . | nindent 4 }}
+ {{- with .Values.serviceAccount.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+{{- end }}
diff --git a/helm/generic-helm-chart/tests/clusterrole_test.yaml b/helm/generic-helm-chart/tests/clusterrole_test.yaml
new file mode 100644
index 0000000000..7cfdf4dc99
--- /dev/null
+++ b/helm/generic-helm-chart/tests/clusterrole_test.yaml
@@ -0,0 +1,271 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+suite: ClusterRole tests
+templates:
+ - clusterrole.yaml
+set:
+ image.repository: example.com/my-operator
+tests:
+ - it: should create ClusterRole when rbac.create is true
+ asserts:
+ - isKind:
+ of: ClusterRole
+ - hasDocuments:
+ count: 1
+
+ - it: should not create ClusterRole when rbac.create is false
+ set:
+ rbac.create: false
+ asserts:
+ - hasDocuments:
+ count: 0
+
+ - it: should always include event permissions
+ asserts:
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
+
+ - it: should add primary resource rules with default verbs
+ set:
+ primaryResources:
+ - apiGroup: "example.com"
+ resources:
+ - myresources
+ asserts:
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - "example.com"
+ resources:
+ - myresources
+ verbs:
+ - get
+ - list
+ - watch
+ - patch
+ - update
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - "example.com"
+ resources:
+ - myresources/status
+ verbs:
+ - get
+ - patch
+ - update
+
+ - it: should use custom verbs for primary resources
+ set:
+ primaryResources:
+ - apiGroup: "example.com"
+ resources:
+ - myresources
+ verbs:
+ - get
+ - list
+ asserts:
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - "example.com"
+ resources:
+ - myresources
+ verbs:
+ - get
+ - list
+
+ - it: should use custom status verbs for primary resources
+ set:
+ primaryResources:
+ - apiGroup: "example.com"
+ resources:
+ - myresources
+ statusVerbs:
+ - get
+ asserts:
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - "example.com"
+ resources:
+ - myresources/status
+ verbs:
+ - get
+
+ - it: should add secondary resource rules with default verbs
+ set:
+ secondaryResources:
+ - apiGroup: ""
+ resources:
+ - configmaps
+ - secrets
+ asserts:
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - ""
+ resources:
+ - configmaps
+ - secrets
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+
+ - it: should use custom verbs for secondary resources
+ set:
+ secondaryResources:
+ - apiGroup: ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - list
+ - watch
+ asserts:
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - list
+ - watch
+
+ - it: should not include lease rules when leader election is disabled
+ asserts:
+ - notContains:
+ path: rules
+ content:
+ apiGroups:
+ - coordination.k8s.io
+ any: true
+
+ - it: should include lease rules when leader election is enabled
+ set:
+ leaderElection.enabled: true
+ asserts:
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+
+ - it: should handle multiple primary and secondary resources
+ set:
+ primaryResources:
+ - apiGroup: "example.com"
+ resources:
+ - foos
+ - apiGroup: "other.io"
+ resources:
+ - bars
+ secondaryResources:
+ - apiGroup: ""
+ resources:
+ - configmaps
+ - apiGroup: "apps"
+ resources:
+ - deployments
+ asserts:
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - "example.com"
+ resources:
+ - foos
+ verbs:
+ - get
+ - list
+ - watch
+ - patch
+ - update
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - "other.io"
+ resources:
+ - bars
+ verbs:
+ - get
+ - list
+ - watch
+ - patch
+ - update
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - ""
+ resources:
+ - configmaps
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
+ - contains:
+ path: rules
+ content:
+ apiGroups:
+ - "apps"
+ resources:
+ - deployments
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+ - delete
diff --git a/helm/generic-helm-chart/tests/clusterrolebinding_test.yaml b/helm/generic-helm-chart/tests/clusterrolebinding_test.yaml
new file mode 100644
index 0000000000..fc358beb21
--- /dev/null
+++ b/helm/generic-helm-chart/tests/clusterrolebinding_test.yaml
@@ -0,0 +1,64 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+suite: ClusterRoleBinding tests
+templates:
+ - clusterrolebinding.yaml
+set:
+ image.repository: example.com/my-operator
+tests:
+ - it: should create ClusterRoleBinding when rbac.create is true
+ asserts:
+ - isKind:
+ of: ClusterRoleBinding
+ - hasDocuments:
+ count: 1
+
+ - it: should not create ClusterRoleBinding when rbac.create is false
+ set:
+ rbac.create: false
+ asserts:
+ - hasDocuments:
+ count: 0
+
+ - it: should reference the correct ClusterRole
+ asserts:
+ - equal:
+ path: roleRef.kind
+ value: ClusterRole
+ - equal:
+ path: roleRef.name
+ value: RELEASE-NAME-generic-operator-chart
+ - equal:
+ path: roleRef.apiGroup
+ value: rbac.authorization.k8s.io
+
+ - it: should reference the correct ServiceAccount
+ asserts:
+ - equal:
+ path: subjects[0].kind
+ value: ServiceAccount
+ - equal:
+ path: subjects[0].name
+ value: RELEASE-NAME-generic-operator-chart
+
+ - it: should use custom service account name
+ set:
+ serviceAccount.name: "my-sa"
+ asserts:
+ - equal:
+ path: subjects[0].name
+ value: my-sa
diff --git a/helm/generic-helm-chart/tests/configmap_test.yaml b/helm/generic-helm-chart/tests/configmap_test.yaml
new file mode 100644
index 0000000000..03d1114632
--- /dev/null
+++ b/helm/generic-helm-chart/tests/configmap_test.yaml
@@ -0,0 +1,57 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+suite: ConfigMap tests
+templates:
+ - configmap.yaml
+set:
+ image.repository: example.com/my-operator
+tests:
+ - it: should create a ConfigMap
+ asserts:
+ - isKind:
+ of: ConfigMap
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-generic-operator-chart-config
+
+ - it: should contain config.yaml key
+ asserts:
+ - exists:
+ path: data["config.yaml"]
+
+ - it: should contain log4j2.xml key
+ asserts:
+ - exists:
+ path: data["log4j2.xml"]
+
+ - it: should render custom operator config
+ set:
+ operatorConfig:
+ config: |
+ josdk.reconciliation.concurrent-threads: 100
+ asserts:
+ - matchRegex:
+ path: data["config.yaml"]
+ pattern: "concurrent-threads: 100"
+
+ - it: should have standard labels
+ asserts:
+ - exists:
+ path: metadata.labels["helm.sh/chart"]
+ - equal:
+ path: metadata.labels["app.kubernetes.io/managed-by"]
+ value: Helm
diff --git a/helm/generic-helm-chart/tests/deployment_test.yaml b/helm/generic-helm-chart/tests/deployment_test.yaml
new file mode 100644
index 0000000000..bdc8845ae9
--- /dev/null
+++ b/helm/generic-helm-chart/tests/deployment_test.yaml
@@ -0,0 +1,290 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+suite: Deployment tests
+templates:
+ - deployment.yaml
+set:
+ image.repository: example.com/my-operator
+tests:
+ - it: should render a Deployment with default values
+ asserts:
+ - isKind:
+ of: Deployment
+ - equal:
+ path: spec.replicas
+ value: 1
+ - equal:
+ path: spec.template.spec.containers[0].name
+ value: operator
+ - equal:
+ path: spec.template.spec.containers[0].image
+ value: "example.com/my-operator:0.1.0"
+ - equal:
+ path: spec.template.spec.containers[0].imagePullPolicy
+ value: IfNotPresent
+
+ - it: should set custom replica count
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ replicaCount: 3
+ asserts:
+ - equal:
+ path: spec.replicas
+ value: 3
+
+ - it: should set custom image tag
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ image.tag: "1.2.3"
+ asserts:
+ - equal:
+ path: spec.template.spec.containers[0].image
+ value: "example.com/my-operator:1.2.3"
+
+ - it: should inject OPERATOR_NAMESPACE env var
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: OPERATOR_NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+
+ - it: should set WATCH_NAMESPACE when configured
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ operator.watchNamespace: "my-namespace"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: WATCH_NAMESPACE
+ value: "my-namespace"
+
+ - it: should not set WATCH_NAMESPACE when empty
+ asserts:
+ - notContains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: WATCH_NAMESPACE
+ any: true
+
+ - it: should add custom env vars
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ operator.env:
+ - name: MY_VAR
+ value: "my-value"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: MY_VAR
+ value: "my-value"
+
+ - it: should add env var from secret reference
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ operator.env:
+ - name: SECRET_VAR
+ valueFrom:
+ secretKeyRef:
+ name: my-secret
+ key: password
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: SECRET_VAR
+ valueFrom:
+ secretKeyRef:
+ name: my-secret
+ key: password
+
+ - it: should add multiple custom env vars
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ operator.env:
+ - name: VAR_ONE
+ value: "one"
+ - name: VAR_TWO
+ value: "two"
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: VAR_ONE
+ value: "one"
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: VAR_TWO
+ value: "two"
+
+ - it: should mount config volume
+ asserts:
+ - contains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: config
+ mountPath: /config
+ readOnly: true
+ - contains:
+ path: spec.template.spec.volumes
+ content:
+ name: config
+ configMap:
+ name: RELEASE-NAME-generic-operator-chart-config
+
+ - it: should add extra volumes and volume mounts
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ extraVolumes:
+ - name: my-secret
+ secret:
+ secretName: my-secret
+ extraVolumeMounts:
+ - name: my-secret
+ mountPath: /secrets
+ readOnly: true
+ asserts:
+ - contains:
+ path: spec.template.spec.volumes
+ content:
+ name: my-secret
+ secret:
+ secretName: my-secret
+ - contains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: my-secret
+ mountPath: /secrets
+ readOnly: true
+
+ - it: should add extra sidecar containers
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ extraContainers:
+ - name: sidecar
+ image: sidecar:latest
+ asserts:
+ - contains:
+ path: spec.template.spec.containers
+ content:
+ name: sidecar
+ image: sidecar:latest
+
+ - it: should add extra init containers
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ extraInitContainers:
+ - name: init
+ image: init:latest
+ asserts:
+ - contains:
+ path: spec.template.spec.initContainers
+ content:
+ name: init
+ image: init:latest
+
+ - it: should set pod security context
+ asserts:
+ - equal:
+ path: spec.template.spec.securityContext.runAsUser
+ value: 9999
+ - equal:
+ path: spec.template.spec.securityContext.fsGroup
+ value: 9999
+
+ - it: should set container security context
+ asserts:
+ - equal:
+ path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation
+ value: false
+ - equal:
+ path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem
+ value: true
+
+ - it: should set pod annotations
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ podAnnotations:
+ prometheus.io/scrape: "true"
+ asserts:
+ - equal:
+ path: spec.template.metadata.annotations["prometheus.io/scrape"]
+ value: "true"
+
+ - it: should set resource limits and requests
+ asserts:
+ - equal:
+ path: spec.template.spec.containers[0].resources.limits.cpu
+ value: 500m
+ - equal:
+ path: spec.template.spec.containers[0].resources.limits.memory
+ value: 512Mi
+ - equal:
+ path: spec.template.spec.containers[0].resources.requests.cpu
+ value: 100m
+ - equal:
+ path: spec.template.spec.containers[0].resources.requests.memory
+ value: 128Mi
+
+ - it: should set image pull secrets
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ imagePullSecrets:
+ - name: my-registry
+ asserts:
+ - contains:
+ path: spec.template.spec.imagePullSecrets
+ content:
+ name: my-registry
+
+ - it: should use fullnameOverride for service account reference
+ documentSelector:
+ path: kind
+ value: Deployment
+ set:
+ fullnameOverride: "my-operator"
+ asserts:
+ - equal:
+ path: spec.template.spec.serviceAccountName
+ value: my-operator
diff --git a/helm/generic-helm-chart/tests/serviceaccount_test.yaml b/helm/generic-helm-chart/tests/serviceaccount_test.yaml
new file mode 100644
index 0000000000..60486cac21
--- /dev/null
+++ b/helm/generic-helm-chart/tests/serviceaccount_test.yaml
@@ -0,0 +1,77 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+suite: ServiceAccount tests
+templates:
+ - serviceaccount.yaml
+set:
+ image.repository: example.com/my-operator
+tests:
+ - it: should create ServiceAccount when serviceAccount.create is true
+ asserts:
+ - isKind:
+ of: ServiceAccount
+ - hasDocuments:
+ count: 1
+
+ - it: should not create ServiceAccount when serviceAccount.create is false
+ set:
+ serviceAccount.create: false
+ asserts:
+ - hasDocuments:
+ count: 0
+
+ - it: should use fullname as default name
+ asserts:
+ - equal:
+ path: metadata.name
+ value: RELEASE-NAME-generic-operator-chart
+
+ - it: should use custom name when provided
+ set:
+ serviceAccount.name: "my-sa"
+ asserts:
+ - equal:
+ path: metadata.name
+ value: my-sa
+
+ - it: should add annotations when provided
+ set:
+ serviceAccount.annotations:
+ eks.amazonaws.com/role-arn: "arn:aws:iam::role/my-role"
+ asserts:
+ - equal:
+ path: metadata.annotations["eks.amazonaws.com/role-arn"]
+ value: "arn:aws:iam::role/my-role"
+
+ - it: should not have annotations by default
+ asserts:
+ - notExists:
+ path: metadata.annotations
+
+ - it: should have standard labels
+ asserts:
+ - exists:
+ path: metadata.labels["helm.sh/chart"]
+ - equal:
+ path: metadata.labels["app.kubernetes.io/name"]
+ value: generic-operator-chart
+ - equal:
+ path: metadata.labels["app.kubernetes.io/instance"]
+ value: RELEASE-NAME
+ - equal:
+ path: metadata.labels["app.kubernetes.io/managed-by"]
+ value: Helm
diff --git a/helm/generic-helm-chart/values.yaml b/helm/generic-helm-chart/values.yaml
new file mode 100644
index 0000000000..8ab452059c
--- /dev/null
+++ b/helm/generic-helm-chart/values.yaml
@@ -0,0 +1,130 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Default values for generic JOSDK operator chart
+
+replicaCount: 1
+
+image:
+ repository: "" # REQUIRED: set to your operator image
+ pullPolicy: IfNotPresent
+ tag: "" # Overrides the image tag whose default is the chart appVersion
+
+imagePullSecrets: []
+nameOverride: ""
+fullnameOverride: ""
+
+serviceAccount:
+ create: true
+ annotations: {}
+ name: ""
+
+podAnnotations: {}
+
+podSecurityContext:
+ fsGroup: 9999
+ runAsUser: 9999
+ runAsGroup: 9999
+
+securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - ALL
+ readOnlyRootFilesystem: true
+
+resources:
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+
+# Leader election configuration
+leaderElection:
+ enabled: false
+
+# Operator configuration
+operator:
+ # Namespace to watch (empty for all namespaces)
+ watchNamespace: ""
+ # Additional environment variables for the operator container
+ env: []
+ # - name: MY_VAR
+ # value: "my-value"
+
+# Operator config files mounted at /config/
+operatorConfig:
+ # Operator configuration YAML (config.yaml)
+ config: |
+ # see options here: https://javaoperatorsdk.io/docs/documentation/configuration/#loading-configuration-from-external-sources
+ # Log4j2 configuration XML (log4j2.xml)
+ log4j2: |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# Primary resources - custom resources that the operator reconciles
+# These get full CRUD + status permissions in the ClusterRole
+primaryResources: []
+# - apiGroup: "example.com"
+# resources:
+# - myresources
+# # optional: custom verbs (defaults to get, list, watch, patch, update)
+# verbs: []
+# # optional: custom status verbs (defaults to get, patch, update)
+# statusVerbs: []
+
+# Secondary resources - resources that the operator creates/manages
+# These get full CRUD permissions in the ClusterRole
+secondaryResources: []
+# - apiGroup: ""
+# resources:
+# - configmaps
+# - secrets
+# # optional: custom verbs (defaults to get, list, watch, create, update, patch, delete)
+# verbs: []
+
+# Additional containers to add to the operator pod (sidecars)
+extraContainers: []
+
+# Additional init containers to add to the operator pod
+extraInitContainers: []
+
+# Additional volumes to add to the operator pod
+extraVolumes: []
+
+# Additional volume mounts to add to the operator container
+extraVolumeMounts: []
+
+# RBAC configuration
+rbac:
+ create: true
diff --git a/helm/run-tests.sh b/helm/run-tests.sh
new file mode 100755
index 0000000000..a3366c28b9
--- /dev/null
+++ b/helm/run-tests.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PLUGIN_NAME="unittest"
+PLUGIN_URL="https://github.com/helm-unittest/helm-unittest"
+
+# Install helm-unittest plugin if not already installed
+if ! helm plugin list | grep -q "${PLUGIN_NAME}"; then
+ echo "Installing helm-unittest plugin..."
+ helm plugin install --verify=false "${PLUGIN_URL}"
+fi
+
+echo "Running helm unit tests..."
+helm unittest "${SCRIPT_DIR}/generic-helm-chart"
diff --git a/sample-operators/metrics-processing/k8s/operator.yaml b/sample-operators/metrics-processing/k8s/operator.yaml
deleted file mode 100644
index 336d587a5b..0000000000
--- a/sample-operators/metrics-processing/k8s/operator.yaml
+++ /dev/null
@@ -1,84 +0,0 @@
-#
-# Copyright Java Operator SDK Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-apiVersion: v1
-kind: ServiceAccount
-metadata:
- name: metrics-processing-operator
-
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: metrics-processing-operator
-spec:
- selector:
- matchLabels:
- app: metrics-processing-operator
- replicas: 1
- template:
- metadata:
- labels:
- app: metrics-processing-operator
- spec:
- serviceAccountName: metrics-processing-operator
- containers:
- - name: operator
- image: metrics-processing-operator
- imagePullPolicy: Never
-
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
- name: metrics-processing-operator-admin
-subjects:
-- kind: ServiceAccount
- name: metrics-processing-operator
- namespace: default
-roleRef:
- kind: ClusterRole
- name: metrics-processing-operator
- apiGroup: rbac.authorization.k8s.io
-
----
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
- name: metrics-processing-operator
-rules:
-- apiGroups:
- - ""
- resources:
- - pods
- verbs:
- - '*'
-- apiGroups:
- - "apiextensions.k8s.io"
- resources:
- - customresourcedefinitions
- verbs:
- - '*'
-- apiGroups:
- - "sample.javaoperatorsdk"
- resources:
- - metricshandlingcustomresource1s
- - metricshandlingcustomresource1s/status
- - metricshandlingcustomresource2s
- - metricshandlingcustomresource2s/status
- verbs:
- - '*'
-
diff --git a/sample-operators/metrics-processing/pom.xml b/sample-operators/metrics-processing/pom.xml
index 752bec3e6a..00adf4fec7 100644
--- a/sample-operators/metrics-processing/pom.xml
+++ b/sample-operators/metrics-processing/pom.xml
@@ -101,6 +101,11 @@
metrics-processing-operator
+
+
+ -Dlog4j.configurationFile=/config/log4j2.xml
+
+
diff --git a/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler1.java b/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler1.java
index ed6d65a4a9..3234deedaf 100644
--- a/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler1.java
+++ b/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler1.java
@@ -26,10 +26,14 @@
import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource;
import io.javaoperatorsdk.operator.sample.metrics.customresource.MetricsHandlingCustomResource1;
-@ControllerConfiguration
+import static io.javaoperatorsdk.operator.sample.metrics.MetricsHandlingReconciler1.NAME;
+
+@ControllerConfiguration(name = NAME)
public class MetricsHandlingReconciler1
extends AbstractMetricsHandlingReconciler {
+ public static final String NAME = "MetricsHandlingReconciler1";
+
private static final long TIMER_DELAY = 5000;
private final TimerEventSource timerEventSource;
diff --git a/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler2.java b/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler2.java
index 2c77c7f5fb..0484d2848e 100644
--- a/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler2.java
+++ b/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingReconciler2.java
@@ -22,6 +22,8 @@
public class MetricsHandlingReconciler2
extends AbstractMetricsHandlingReconciler {
+ public static final String NAME = "MetricsHandlingReconciler2";
+
public MetricsHandlingReconciler2() {
super(150);
}
diff --git a/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingSampleOperator.java b/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingSampleOperator.java
index 9f563bec9c..2c6b9c3e90 100644
--- a/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingSampleOperator.java
+++ b/sample-operators/metrics-processing/src/main/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingSampleOperator.java
@@ -17,7 +17,9 @@
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Path;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -29,6 +31,11 @@
import io.javaoperatorsdk.operator.Operator;
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
+import io.javaoperatorsdk.operator.config.loader.ConfigLoader;
+import io.javaoperatorsdk.operator.config.loader.ConfigProvider;
+import io.javaoperatorsdk.operator.config.loader.provider.AggregatePriorityListConfigProvider;
+import io.javaoperatorsdk.operator.config.loader.provider.EnvVarConfigProvider;
+import io.javaoperatorsdk.operator.config.loader.provider.YamlConfigProvider;
import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetricsV2;
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.MeterRegistry;
@@ -48,13 +55,6 @@ public class MetricsHandlingSampleOperator {
private static final Logger log = LoggerFactory.getLogger(MetricsHandlingSampleOperator.class);
- public static boolean isLocal() {
- String deployment = System.getProperty("test.deployment");
- boolean remote = (deployment != null && deployment.equals("remote"));
- log.info("Running the operator {} ", remote ? "remotely" : "locally");
- return !remote;
- }
-
/**
* Based on env variables a different flavor of Reconciler is used, showcasing how the same logic
* can be implemented using the low level and higher level APIs.
@@ -62,11 +62,20 @@ public static boolean isLocal() {
public static void main(String[] args) {
log.info("Metrics Handling Sample Operator starting!");
+ var configProviders = new ArrayList();
+ configProviders.add(new EnvVarConfigProvider());
+ configProviders.add(new YamlConfigProvider(Path.of("/config/config.yaml")));
+ var configLoader = new ConfigLoader(new AggregatePriorityListConfigProvider(configProviders));
+
Metrics metrics = initOTLPMetrics(isLocal());
Operator operator =
- new Operator(o -> o.withStopOnInformerErrorDuringStartup(false).withMetrics(metrics));
- operator.register(new MetricsHandlingReconciler1());
- operator.register(new MetricsHandlingReconciler2());
+ new Operator(o -> configLoader.applyConfigs().andThen(k -> k.withMetrics(metrics)));
+ operator.register(
+ new MetricsHandlingReconciler1(),
+ configLoader.applyControllerConfigs(MetricsHandlingReconciler1.NAME));
+ operator.register(
+ new MetricsHandlingReconciler2(),
+ configLoader.applyControllerConfigs(MetricsHandlingReconciler2.NAME));
operator.start();
}
@@ -148,4 +157,12 @@ private static Map loadConfigFromYaml() {
}
return configMap;
}
+
+ // only for testing purposes
+ public static boolean isLocal() {
+ String deployment = System.getProperty("test.deployment");
+ boolean remote = (deployment != null && deployment.equals("remote"));
+ log.info("Running the operator {} ", remote ? "remotely" : "locally");
+ return !remote;
+ }
}
diff --git a/sample-operators/metrics-processing/src/test/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingE2E.java b/sample-operators/metrics-processing/src/test/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingE2E.java
index 40924c572d..34e96a5870 100644
--- a/sample-operators/metrics-processing/src/test/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingE2E.java
+++ b/sample-operators/metrics-processing/src/test/java/io/javaoperatorsdk/operator/sample/metrics/MetricsHandlingE2E.java
@@ -59,13 +59,14 @@ class MetricsHandlingE2E {
static final int OTEL_COLLECTOR_PORT = 4318;
public static final Duration TEST_DURATION = Duration.ofSeconds(60);
public static final String NAME_LABEL_KEY = "app.kubernetes.io/name";
+ static final String HELM_RELEASE_NAME = "metrics-processing";
private LocalPortForward prometheusPortForward;
private LocalPortForward otelCollectorPortForward;
static final KubernetesClient client = new KubernetesClientBuilder().build();
- MetricsHandlingE2E() throws FileNotFoundException {}
+ MetricsHandlingE2E() {}
@RegisterExtension
AbstractOperatorExtension operator =
@@ -76,18 +77,15 @@ class MetricsHandlingE2E {
.withConfigurationService(
c -> c.withMetrics(MetricsHandlingSampleOperator.initOTLPMetrics(true)))
.build()
- : ClusterDeployedOperatorExtension.builder()
- .withOperatorDeployment(
- new KubernetesClientBuilder()
- .build()
- .load(new FileInputStream("k8s/operator.yaml"))
- .items())
- .build();
+ : ClusterDeployedOperatorExtension.builder().build();
@BeforeAll
- void setupObservability() {
+ void setup() {
log.info("Setting up observability stack...");
installObservabilityServices();
+ if (!isLocal()) {
+ helmInstall();
+ }
prometheusPortForward = portForward(NAME_LABEL_KEY, "prometheus", PROMETHEUS_PORT);
if (isLocal()) {
otelCollectorPortForward =
@@ -97,10 +95,67 @@ void setupObservability() {
@AfterAll
void cleanup() throws IOException {
+ if (!isLocal()) {
+ helmUninstall();
+ }
closePortForward(prometheusPortForward);
closePortForward(otelCollectorPortForward);
}
+ private void helmInstall() {
+ try {
+ var chartPath =
+ findProjectRoot("helm").toPath().resolve("helm/generic-helm-chart").toString();
+ var valuesUrl = MetricsHandlingE2E.class.getClassLoader().getResource("helm-values.yaml");
+ if (valuesUrl == null) {
+ throw new IllegalStateException("helm-values.yaml not found on classpath");
+ }
+ var valuesPath = new File(valuesUrl.toURI()).getAbsolutePath();
+ var namespace = getNamespace();
+
+ log.info("Installing helm release '{}' into namespace '{}'", HELM_RELEASE_NAME, namespace);
+ runCommand(
+ "helm",
+ "install",
+ HELM_RELEASE_NAME,
+ chartPath,
+ "-f",
+ valuesPath,
+ "--namespace",
+ namespace,
+ "--wait",
+ "--timeout",
+ "2m");
+ log.info("Helm release '{}' installed successfully", HELM_RELEASE_NAME);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to install helm chart", e);
+ }
+ }
+
+ private String getNamespace() {
+ var ns = operator.getNamespace();
+ return ns == null ? "default" : ns;
+ }
+
+ private void helmUninstall() {
+ try {
+ var namespace = getNamespace();
+ log.info("Uninstalling helm release '{}' from namespace '{}'", HELM_RELEASE_NAME, namespace);
+ runCommand(
+ "helm",
+ "uninstall",
+ HELM_RELEASE_NAME,
+ "--namespace",
+ namespace,
+ "--wait",
+ "--timeout",
+ "2m");
+ log.info("Helm release '{}' uninstalled successfully", HELM_RELEASE_NAME);
+ } catch (Exception e) {
+ log.warn("Failed to uninstall helm release", e);
+ }
+ }
+
private LocalPortForward portForward(String labelKey, String labelValue, int port) {
log.info("Waiting for pod with label {}={} to be ready...", labelKey, labelValue);
AtomicReference portForwardPod = new AtomicReference<>();
@@ -307,43 +362,43 @@ private String queryPrometheus(String prometheusUrl, String query) throws IOExce
private void installObservabilityServices() {
try {
- // Find the observability script relative to project root
- File projectRoot = new File(".").getCanonicalFile();
- while (projectRoot != null && !new File(projectRoot, "observability").exists()) {
- projectRoot = projectRoot.getParentFile();
- }
-
- if (projectRoot == null) {
- throw new IllegalStateException("Could not find observability directory");
- }
-
- File scriptFile = new File(projectRoot, "observability/install-observability.sh");
- if (!scriptFile.exists()) {
- throw new IllegalStateException(
- "Observability script not found at: " + scriptFile.getAbsolutePath());
- }
+ File scriptFile =
+ findProjectRoot("observability")
+ .toPath()
+ .resolve("observability/install-observability.sh")
+ .toFile();
log.info("Running observability setup script: {}", scriptFile.getAbsolutePath());
+ runCommand("/bin/sh", scriptFile.getAbsolutePath());
+ log.info("Observability stack is ready");
+ } catch (Exception e) {
+ log.error("Failed to setup observability stack", e);
+ throw new RuntimeException(e);
+ }
+ }
- // Run the install-observability.sh script
- ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", scriptFile.getAbsolutePath());
- processBuilder.redirectErrorStream(true);
+ private static File findProjectRoot(String marker) throws IOException {
+ File dir = new File(".").getCanonicalFile();
+ while (dir != null && !new File(dir, marker).exists()) {
+ dir = dir.getParentFile();
+ }
+ if (dir == null) {
+ throw new IllegalStateException("Could not find '" + marker + "' directory in project root");
+ }
+ return dir;
+ }
- processBuilder.environment().putAll(System.getenv());
- Process process = processBuilder.start();
- BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+ private static void runCommand(String... command) throws IOException, InterruptedException {
+ var process = new ProcessBuilder(command).redirectErrorStream(true).start();
+ try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
- log.info("Observability setup: {}", line);
+ log.info("{}: {}", command[0], line);
}
-
- int exitCode = process.waitFor();
- if (exitCode != 0) {
- log.warn("Observability setup script returned exit code: {}", exitCode);
- }
- log.info("Observability stack is ready");
- } catch (Exception e) {
- log.error("Failed to setup observability stack", e);
- throw new RuntimeException(e);
+ }
+ int exitCode = process.waitFor();
+ if (exitCode != 0) {
+ throw new IllegalStateException(
+ String.join(" ", command) + " failed with exit code: " + exitCode);
}
}
}
diff --git a/sample-operators/metrics-processing/src/test/resources/helm-values.yaml b/sample-operators/metrics-processing/src/test/resources/helm-values.yaml
new file mode 100644
index 0000000000..bb8e251139
--- /dev/null
+++ b/sample-operators/metrics-processing/src/test/resources/helm-values.yaml
@@ -0,0 +1,35 @@
+#
+# Copyright Java Operator SDK Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Helm values for metrics-processing operator E2E test deployment
+# Used with the generic-operator-chart from helm/generic-helm-chart/
+
+image:
+ repository: metrics-processing-operator
+ pullPolicy: Never
+ tag: "latest"
+
+nameOverride: "metrics-processing-operator"
+
+resources: {}
+
+# Primary resources - custom resources that the operator reconciles
+primaryResources:
+ - apiGroup: "sample.javaoperatorsdk"
+ resources:
+ - metricshandlingcustomresource1s
+ - metricshandlingcustomresource2s
+