executionTimerConfig) {
+ this.executionTimerConfig = executionTimerConfig;
+ return this;
+ }
+
+ /**
+ * When enabled, a {@code namespace} tag is added to all per-reconciliation counters (started,
+ * success, failure, retries, events, deletes). Gauges remain controller-scoped because
+ * namespaces are not known at controller registration time.
+ *
+ * Disabled by default to avoid unexpected cardinality increases in existing deployments.
+ *
+ * @return this builder for method chaining
+ */
+ public MicrometerMetricsV2Builder withNamespaceAsTag() {
+ this.includeNamespaceTag = true;
+ return this;
+ }
+
+ public MicrometerMetricsV2 build() {
+ return new MicrometerMetricsV2(registry, executionTimerConfig, includeNamespaceTag);
+ }
+ }
+}
diff --git a/observability/README.md b/observability/README.md
new file mode 100644
index 0000000000..58caae27d0
--- /dev/null
+++ b/observability/README.md
@@ -0,0 +1,252 @@
+# Observability Stack for Java Operator SDK
+
+This directory contains the setup scripts and Grafana dashboards for monitoring Java Operator SDK applications.
+
+## Installation
+
+Run the installation script to deploy the full observability stack (OpenTelemetry Collector, Prometheus, and Grafana):
+
+```bash
+./install-observability.sh
+```
+
+This will install:
+- **cert-manager** - Required for OpenTelemetry Operator
+- **OpenTelemetry Operator** - Manages OpenTelemetry Collector instances
+- **OpenTelemetry Collector** - Receives OTLP metrics and exports to Prometheus
+- **Prometheus** - Metrics storage and querying
+- **Grafana** - Metrics visualization
+
+## Accessing Services
+
+### Grafana
+```bash
+kubectl port-forward -n observability svc/kube-prometheus-stack-grafana 3000:80
+```
+Then open http://localhost:3000
+- Username: `admin`
+- Password: `admin`
+
+### Prometheus
+```bash
+kubectl port-forward -n observability svc/kube-prometheus-stack-prometheus 9090:9090
+```
+Then open http://localhost:9090
+
+## Grafana Dashboards
+
+Two pre-configured dashboards are **automatically imported** during installation:
+
+### 1. JVM Metrics Dashboard (`jvm-metrics-dashboard.json`)
+
+Monitors Java Virtual Machine health and performance:
+
+**Panels:**
+- **JVM Memory Used** - Heap and non-heap memory consumption by memory pool
+- **JVM Threads** - Live, daemon, and peak thread counts
+- **GC Pause Time Rate** - Garbage collection pause duration
+- **GC Pause Count Rate** - Frequency of garbage collection events
+- **CPU Usage** - System CPU utilization percentage
+- **Classes Loaded** - Number of classes currently loaded
+- **Process Uptime** - Application uptime in seconds
+- **CPU Count** - Available processor cores
+- **GC Memory Allocation Rate** - Memory allocation and promotion rates
+- **Heap Memory Max vs Committed** - Heap memory limits and commitments
+
+**Key Metrics:**
+- `jvm.memory.used`, `jvm.memory.max`, `jvm.memory.committed`
+- `jvm.gc.pause`, `jvm.gc.memory.allocated`, `jvm.gc.memory.promoted`
+- `jvm.threads.live`, `jvm.threads.daemon`, `jvm.threads.peak`
+- `jvm.classes.loaded`, `jvm.classes.unloaded`
+- `system.cpu.usage`, `system.cpu.count`
+- `process.uptime`
+
+**Filtering:**
+All panels filter by `service_name="josdk"` to show metrics only from your operator.
+
+### 2. Java Operator SDK Metrics Dashboard (`josdk-operator-metrics-dashboard.json`)
+
+Monitors Kubernetes operator performance and health:
+
+**Panels:**
+- **Reconciliation Rate (Started)** - Rate of reconciliation loops triggered
+- **Reconciliation Success vs Failure Rate** - Success/failure ratio over time
+- **Currently Executing Reconciliations** - Active reconciliation threads
+- **Reconciliation Queue Size** - Pending reconciliation work
+- **Total Reconciliations** - Cumulative count of reconciliations
+- **Error Rate** - Overall error rate across all reconciliations
+- **Reconciliation Execution Time** - P50, P95, P99 latency percentiles
+- **Event Reception Rate** - Kubernetes event processing rate
+- **Failures by Exception Type** - Breakdown of errors by exception class
+- **Controller Execution Success vs Failure** - Controller-level success metrics
+- **Delete Event Rate** - Resource deletion event frequency
+- **Reconciliation Retry Rate** - Retry attempts and patterns
+
+**Key Metrics:**
+- `operator.sdk.reconciliations.started`, `.success`, `.failed`
+- `operator.sdk.reconciliations.executions` - Current execution count
+- `operator.sdk.reconciliations.queue.size` - Queue depth
+- `operator.sdk.controllers.execution.reconcile` - Execution timing histograms
+- `operator.sdk.events.received`, `.delete` - Event reception
+- Retry metrics and failure breakdowns
+
+**Filtering:**
+All panels filter by `service_name="josdk"` to show metrics only from your operator.
+
+## Importing Dashboards into Grafana
+
+### Automatic Import (Default)
+
+The dashboards are **automatically imported** when you run `./install-observability.sh`. They will appear in Grafana within 30-60 seconds after installation. No manual steps required!
+
+To verify the dashboards were imported:
+1. Access Grafana at http://localhost:3000
+2. Navigate to **Dashboards** → **Browse**
+3. Look for "JOSDK - JVM Metrics" and "JOSDK - Operator Metrics"
+
+### Manual Import Methods
+
+If you need to re-import or update the dashboards manually:
+
+#### Method 1: Via Grafana UI
+
+1. Access Grafana at http://localhost:3000
+2. Login with admin/admin
+3. Navigate to **Dashboards** → **Import**
+4. Click **Upload JSON file**
+5. Select `jvm-metrics-dashboard.json` or `josdk-operator-metrics-dashboard.json`
+6. Select **Prometheus** as the data source
+7. Click **Import**
+
+#### Method 2: Via kubectl ConfigMap
+
+```bash
+# Re-import JVM dashboard
+kubectl create configmap jvm-metrics-dashboard \
+ --from-file=jvm-metrics-dashboard.json \
+ -n observability \
+ -o yaml --dry-run=client | \
+ kubectl label --dry-run=client --local -f - grafana_dashboard=1 -o yaml | \
+ kubectl apply -f -
+
+# Re-import Operator dashboard
+kubectl create configmap josdk-operator-metrics-dashboard \
+ --from-file=josdk-operator-metrics-dashboard.json \
+ -n observability \
+ -o yaml --dry-run=client | \
+ kubectl label --dry-run=client --local -f - grafana_dashboard=1 -o yaml | \
+ kubectl apply -f -
+```
+
+The dashboards will be automatically discovered and loaded by Grafana within 30-60 seconds.
+
+## Configuring Your Operator
+
+To enable metrics export from your JOSDK operator, ensure your application:
+
+1. **Has the required dependency** (already included in webpage sample):
+ ```xml
+
+ io.micrometer
+ micrometer-registry-otlp
+
+ ```
+
+2. **Configures OTLP export** via `otlp-config.yaml`:
+ ```yaml
+ otlp:
+ url: "http://otel-collector-collector.observability.svc.cluster.local:4318/v1/metrics"
+ step: 15s
+ batchSize: 15000
+ aggregationTemporality: "cumulative"
+ ```
+
+3. **Registers JVM and JOSDK metrics** (see `WebPageOperator.java` for reference implementation)
+
+## OTLP Endpoints
+
+The OpenTelemetry Collector provides the following endpoints:
+
+- **OTLP gRPC**: `otel-collector-collector.observability.svc.cluster.local:4317`
+- **OTLP HTTP**: `otel-collector-collector.observability.svc.cluster.local:4318`
+- **Prometheus Scrape**: `http://otel-collector-prometheus.observability.svc.cluster.local:8889/metrics`
+
+## Troubleshooting
+
+### Check OpenTelemetry Collector Logs
+```bash
+kubectl logs -n observability -l app.kubernetes.io/name=otel-collector -f
+```
+
+### Check Prometheus Targets
+```bash
+kubectl port-forward -n observability svc/kube-prometheus-stack-prometheus 9090:9090
+```
+Open http://localhost:9090/targets and verify the OTLP collector target is UP.
+
+### Verify Metrics in Prometheus
+Open Prometheus UI and search for metrics:
+- JVM metrics: `jvm_*`
+- Operator metrics: `operator_sdk_*`
+
+### Check Grafana Data Source
+1. Navigate to **Configuration** → **Data Sources**
+2. Verify Prometheus data source is configured and working
+3. Click **Test** to verify connectivity
+
+## Uninstalling
+
+To remove the observability stack:
+
+```bash
+kubectl delete configmap -n observability jvm-metrics-dashboard josdk-operator-metrics-dashboard
+kubectl delete -n observability OpenTelemetryCollector otel-collector
+helm uninstall -n observability kube-prometheus-stack
+helm uninstall -n observability opentelemetry-operator
+helm uninstall -n cert-manager cert-manager
+kubectl delete namespace observability cert-manager
+```
+
+## Customizing Dashboards
+
+The dashboard JSON files can be modified to:
+- Add new panels for custom metrics
+- Adjust time ranges and refresh intervals
+- Change visualization types
+- Add templating variables for filtering
+- Modify alert thresholds
+
+After making changes, re-import the dashboard using one of the methods above.
+
+## Example Queries
+
+### JVM Metrics
+```promql
+# Heap memory usage percentage
+(jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}) * 100
+
+# GC throughput (percentage of time NOT in GC)
+100 - (rate(jvm_gc_pause_seconds_sum[5m]) * 100)
+
+# Thread count trend
+jvm_threads_live_threads
+```
+
+### Operator Metrics
+```promql
+# Reconciliation success rate
+rate(operator_sdk_reconciliations_success_total[5m]) / rate(operator_sdk_reconciliations_started_total[5m])
+
+# Average reconciliation time
+rate(operator_sdk_controllers_execution_reconcile_seconds_sum[5m]) / rate(operator_sdk_controllers_execution_reconcile_seconds_count[5m])
+
+# Queue saturation
+operator_sdk_reconciliations_queue_size / on() group_left() max(operator_sdk_reconciliations_queue_size)
+```
+
+## References
+
+- [Java Operator SDK Documentation](https://javaoperatorsdk.io)
+- [Micrometer OTLP Documentation](https://micrometer.io/docs/registry/otlp)
+- [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/)
+- [Grafana Dashboards](https://grafana.com/docs/grafana/latest/dashboards/)
diff --git a/observability/install-observability.sh b/observability/install-observability.sh
new file mode 100755
index 0000000000..c6c8f96201
--- /dev/null
+++ b/observability/install-observability.sh
@@ -0,0 +1,316 @@
+#!/bin/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 -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+echo -e "${GREEN}========================================${NC}"
+echo -e "${GREEN}Installing Observability Stack${NC}"
+echo -e "${GREEN}OpenTelemetry + Prometheus + Grafana${NC}"
+echo -e "${GREEN}========================================${NC}"
+
+# Check if helm is installed, download locally if not
+echo -e "\n${YELLOW}Checking helm installation...${NC}"
+if ! command -v helm &> /dev/null; then
+ echo -e "${YELLOW}helm not found, downloading locally...${NC}"
+ HELM_INSTALL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.helm"
+ mkdir -p "$HELM_INSTALL_DIR"
+ HELM_BIN="$HELM_INSTALL_DIR/helm"
+ if [ ! -f "$HELM_BIN" ]; then
+ curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 \
+ | HELM_INSTALL_DIR="$HELM_INSTALL_DIR" USE_SUDO=false bash
+ fi
+ export PATH="$HELM_INSTALL_DIR:$PATH"
+ echo -e "${GREEN}✓ helm downloaded to $HELM_BIN${NC}"
+else
+ echo -e "${GREEN}✓ helm is installed${NC}"
+fi
+
+# Add Helm repositories
+echo -e "\n${YELLOW}Adding Helm repositories...${NC}"
+helm repo add jetstack https://charts.jetstack.io
+helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
+helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
+helm repo update
+echo -e "${GREEN}✓ Helm repositories added${NC}"
+
+echo -e "\n${GREEN}========================================${NC}"
+echo -e "${GREEN}Installing Components (Parallel)${NC}"
+echo -e "${GREEN}========================================${NC}"
+echo -e "The following will be installed:"
+echo -e " • cert-manager"
+echo -e " • OpenTelemetry Operator"
+echo -e " • Prometheus & Grafana"
+echo -e " • OpenTelemetry Collector"
+echo -e " • Service Monitors"
+echo -e "\n${YELLOW}All resources will be applied first, then we'll wait for them to become ready.${NC}\n"
+
+# Install cert-manager (required for OpenTelemetry Operator)
+echo -e "\n${YELLOW}Installing cert-manager...${NC}"
+if kubectl get namespace cert-manager > /dev/null 2>&1; then
+ echo -e "${YELLOW}cert-manager namespace already exists, skipping...${NC}"
+else
+ kubectl create namespace cert-manager
+ helm install cert-manager jetstack/cert-manager \
+ --namespace cert-manager \
+ --set crds.enabled=true
+ echo -e "${GREEN}✓ cert-manager installation started${NC}"
+fi
+
+# Create observability namespace
+echo -e "\n${YELLOW}Creating observability namespace...${NC}"
+kubectl create namespace observability --dry-run=client -o yaml | kubectl apply -f -
+echo -e "${GREEN}✓ observability namespace ready${NC}"
+
+# Install OpenTelemetry Operator
+echo -e "\n${YELLOW}Installing OpenTelemetry Operator...${NC}"
+
+if helm list -n observability | grep -q opentelemetry-operator; then
+ echo -e "${YELLOW}OpenTelemetry Operator already installed, upgrading...${NC}"
+ helm upgrade opentelemetry-operator open-telemetry/opentelemetry-operator \
+ --namespace observability \
+ --set "manager.collectorImage.repository=otel/opentelemetry-collector-contrib"
+else
+ helm install opentelemetry-operator open-telemetry/opentelemetry-operator \
+ --namespace observability \
+ --set "manager.collectorImage.repository=otel/opentelemetry-collector-contrib"
+fi
+echo -e "${GREEN}✓ OpenTelemetry Operator installation started${NC}"
+
+# Install kube-prometheus-stack (includes Prometheus + Grafana)
+echo -e "\n${YELLOW}Installing Prometheus and Grafana stack...${NC}"
+if helm list -n observability | grep -q kube-prometheus-stack; then
+ echo -e "${YELLOW}kube-prometheus-stack already installed, upgrading...${NC}"
+ helm upgrade kube-prometheus-stack prometheus-community/kube-prometheus-stack \
+ --namespace observability \
+ --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false \
+ --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \
+ --set grafana.adminPassword=admin
+else
+ helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
+ --namespace observability \
+ --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false \
+ --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \
+ --set grafana.adminPassword=admin
+fi
+echo -e "${GREEN}✓ Prometheus and Grafana installation started${NC}"
+
+# Create OpenTelemetry Collector instance
+echo -e "\n${YELLOW}Creating OpenTelemetry Collector...${NC}"
+cat </dev/null || echo -e "${YELLOW}cert-manager already running or skipped${NC}"
+
+# Wait for observability pods
+echo -e "${YELLOW}Checking observability pods...${NC}"
+kubectl wait --for=condition=ready pod --all -n observability --timeout=300s
+
+echo -e "${GREEN}✓ All pods are ready${NC}"
+
+# Import Grafana dashboards
+echo -e "\n${YELLOW}Importing Grafana dashboards...${NC}"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+if [ -f "$SCRIPT_DIR/jvm-metrics-dashboard.json" ]; then
+ kubectl create configmap jvm-metrics-dashboard \
+ --from-file="$SCRIPT_DIR/jvm-metrics-dashboard.json" \
+ -n observability \
+ --dry-run=client -o yaml | \
+ kubectl label --dry-run=client --local -f - grafana_dashboard=1 -o yaml | \
+ kubectl apply -f -
+ echo -e "${GREEN}✓ JVM Metrics dashboard imported${NC}"
+else
+ echo -e "${YELLOW}⚠ JVM Metrics dashboard not found at $SCRIPT_DIR/jvm-metrics-dashboard.json${NC}"
+fi
+
+if [ -f "$SCRIPT_DIR/josdk-operator-metrics-dashboard.json" ]; then
+ kubectl create configmap josdk-operator-metrics-dashboard \
+ --from-file="$SCRIPT_DIR/josdk-operator-metrics-dashboard.json" \
+ -n observability \
+ --dry-run=client -o yaml | \
+ kubectl label --dry-run=client --local -f - grafana_dashboard=1 -o yaml | \
+ kubectl apply -f -
+ echo -e "${GREEN}✓ JOSDK Operator Metrics dashboard imported${NC}"
+else
+ echo -e "${YELLOW}⚠ JOSDK Operator Metrics dashboard not found at $SCRIPT_DIR/josdk-operator-metrics-dashboard.json${NC}"
+fi
+
+echo -e "${GREEN}✓ Dashboards will be available in Grafana shortly${NC}"
+
+# Get pod statuses
+echo -e "\n${GREEN}========================================${NC}"
+echo -e "${GREEN}Installation Complete!${NC}"
+echo -e "${GREEN}========================================${NC}"
+
+echo -e "\n${YELLOW}Pod Status:${NC}"
+kubectl get pods -n observability
+
+echo -e "\n${GREEN}========================================${NC}"
+echo -e "${GREEN}Access Information${NC}"
+echo -e "${GREEN}========================================${NC}"
+
+echo -e "\n${YELLOW}Grafana:${NC}"
+echo -e " Username: ${GREEN}admin${NC}"
+echo -e " Password: ${GREEN}admin${NC}"
+echo -e " Access with: ${GREEN}kubectl port-forward -n observability svc/kube-prometheus-stack-grafana 3000:80${NC}"
+echo -e " Then open: ${GREEN}http://localhost:3000${NC}"
+
+echo -e "\n${YELLOW}Prometheus:${NC}"
+echo -e " Access with: ${GREEN}kubectl port-forward -n observability svc/kube-prometheus-stack-prometheus 9090:9090${NC}"
+echo -e " Then open: ${GREEN}http://localhost:9090${NC}"
+
+echo -e "\n${YELLOW}OpenTelemetry Collector:${NC}"
+echo -e " OTLP gRPC endpoint: ${GREEN}otel-collector-collector.observability.svc.cluster.local:4317${NC}"
+echo -e " OTLP HTTP endpoint: ${GREEN}otel-collector-collector.observability.svc.cluster.local:4318${NC}"
+echo -e " Prometheus metrics: ${GREEN}http://otel-collector-prometheus.observability.svc.cluster.local:8889/metrics${NC}"
+
+echo -e "\n${YELLOW}Configure your Java Operator to use OpenTelemetry:${NC}"
+echo -e " Add dependency: ${GREEN}io.javaoperatorsdk:operator-framework-opentelemetry-support${NC}"
+echo -e " Set environment variables:"
+echo -e " ${GREEN}OTEL_SERVICE_NAME=your-operator-name${NC}"
+echo -e " ${GREEN}OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector-collector.observability.svc.cluster.local:4318${NC}"
+echo -e " ${GREEN}OTEL_METRICS_EXPORTER=otlp${NC}"
+echo -e " ${GREEN}OTEL_TRACES_EXPORTER=otlp${NC}"
+
+echo -e "\n${GREEN}========================================${NC}"
+echo -e "${GREEN}Grafana Dashboards${NC}"
+echo -e "${GREEN}========================================${NC}"
+echo -e "\nAutomatically imported dashboards:"
+echo -e " - ${GREEN}JOSDK - JVM Metrics${NC} - Java Virtual Machine health and performance"
+echo -e " - ${GREEN}JOSDK - Operator Metrics${NC} - Kubernetes operator performance and reconciliation"
+echo -e "\nPre-installed Kubernetes dashboards:"
+echo -e " - Kubernetes / Compute Resources / Cluster"
+echo -e " - Kubernetes / Compute Resources / Namespace (Pods)"
+echo -e " - Node Exporter / Nodes"
+echo -e "\n${YELLOW}Note:${NC} Dashboards may take 30-60 seconds to appear in Grafana after installation."
+
+echo -e "\n${YELLOW}To uninstall:${NC}"
+echo -e " kubectl delete configmap -n observability jvm-metrics-dashboard josdk-operator-metrics-dashboard"
+echo -e " kubectl delete -n observability OpenTelemetryCollector otel-collector"
+echo -e " helm uninstall -n observability kube-prometheus-stack"
+echo -e " helm uninstall -n observability opentelemetry-operator"
+echo -e " helm uninstall -n cert-manager cert-manager"
+echo -e " kubectl delete namespace observability cert-manager"
+
+echo -e "\n${GREEN}Done!${NC}"
diff --git a/observability/josdk-operator-metrics-dashboard.json b/observability/josdk-operator-metrics-dashboard.json
new file mode 100644
index 0000000000..3c0c76a3db
--- /dev/null
+++ b/observability/josdk-operator-metrics-dashboard.json
@@ -0,0 +1,1097 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "grafana",
+ "uid": "-- Grafana --"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": null,
+ "links": [],
+ "liveNow": false,
+ "panels": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Rate of reconciliations started per second",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 0
+ },
+ "id": 1,
+ "options": {
+ "legend": {
+ "calcs": ["last", "mean"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(reconciliations_started_total{service_name=~\"$service_name\"}[5m])) by (controller_name)",
+ "legendFormat": "{{controller_name}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Reconciliation Rate (Started)",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Success vs Failure rate of reconciliations",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Success"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "green",
+ "mode": "fixed"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Failure"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "red",
+ "mode": "fixed"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 0
+ },
+ "id": 2,
+ "options": {
+ "legend": {
+ "calcs": ["last", "mean"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(reconciliations_success_total{service_name=~\"$service_name\"}[5m])) by (controller_name)",
+ "legendFormat": "Success - {{controller_name}}",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(reconciliations_failure_total{service_name=~\"$service_name\"}[5m])) by (controller_name)",
+ "legendFormat": "Failure - {{controller_name}}",
+ "range": true,
+ "refId": "B"
+ }
+ ],
+ "title": "Reconciliation Success vs Failure Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Current number of reconciliations being executed",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "yellow",
+ "value": 5
+ },
+ {
+ "color": "red",
+ "value": 10
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 0,
+ "y": 8
+ },
+ "id": 3,
+ "options": {
+ "orientation": "auto",
+ "reduceOptions": {
+ "values": false,
+ "calcs": ["lastNotNull"],
+ "fields": ""
+ },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(reconciliations_executions{service_name=~\"$service_name\"})",
+ "legendFormat": "Executing",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Currently Executing Reconciliations",
+ "type": "gauge"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Number of custom resources submitted to reconciliation to executor service",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "yellow",
+ "value": 10
+ },
+ {
+ "color": "red",
+ "value": 50
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 6,
+ "y": 8
+ },
+ "id": 4,
+ "options": {
+ "orientation": "auto",
+ "reduceOptions": {
+ "values": false,
+ "calcs": ["lastNotNull"],
+ "fields": ""
+ },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(reconciliations_active{service_name=~\"$service_name\"})",
+ "legendFormat": "Active",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Reconciliation queue size",
+ "type": "gauge"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Total reconciliations started",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "blue",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 12,
+ "y": 8
+ },
+ "id": 5,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "values": false,
+ "calcs": ["lastNotNull"],
+ "fields": ""
+ },
+ "textMode": "auto"
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(reconciliations_started_total{service_name=~\"$service_name\"})",
+ "legendFormat": "Total",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Total Reconciliations",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Error rate by exception type",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 1
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 18,
+ "y": 8
+ },
+ "id": 6,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "values": false,
+ "calcs": ["lastNotNull"],
+ "fields": ""
+ },
+ "textMode": "auto"
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(reconciliations_failure_total{service_name=~\"$service_name\"}[5m]))",
+ "legendFormat": "Error Rate",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Error Rate",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Number of custom resources tracked by controller",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "blue",
+ "value": null
+ },
+ {
+ "color": "green",
+ "value": 10
+ },
+ {
+ "color": "yellow",
+ "value": 100
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 16
+ },
+ "id": 13,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "horizontal",
+ "reduceOptions": {
+ "values": false,
+ "calcs": ["lastNotNull"],
+ "fields": ""
+ },
+ "textMode": "auto"
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "custom_resources{service_name=~\"$service_name\"}",
+ "legendFormat": "{{controller_name}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Custom Resources Count",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Controller execution time percentiles",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 16
+ },
+ "id": 7,
+ "options": {
+ "legend": {
+ "calcs": ["last", "max"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.50, sum(rate(reconciliations_execution_seconds_bucket{service_name=~\"$service_name\"}[5m])) by (le, controller_name))",
+ "legendFormat": "p50 - {{controller_name}}",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.95, sum(rate(reconciliations_execution_seconds_bucket{service_name=~\"$service_name\"}[5m])) by (le, controller_name))",
+ "legendFormat": "p95 - {{controller_name}}",
+ "range": true,
+ "refId": "B"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.99, sum(rate(reconciliations_execution_seconds_bucket{service_name=~\"$service_name\"}[5m])) by (le, controller_name))",
+ "legendFormat": "p99 - {{controller_name}}",
+ "range": true,
+ "refId": "C"
+ }
+ ],
+ "title": "Reconciliation Execution Time (Percentiles)",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Rate of events received by the operator",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 24
+ },
+ "id": 8,
+ "options": {
+ "legend": {
+ "calcs": ["last", "mean"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(events_received_total{service_name=~\"$service_name\"}[5m])) by (event, action)",
+ "legendFormat": "{{event}} - {{action}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Event Reception Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Failures by controller",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 24
+ },
+ "id": 9,
+ "options": {
+ "legend": {
+ "calcs": ["last", "sum"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(reconciliations_failure_total{service_name=~\"$service_name\"}[5m])) by (controller_name)",
+ "legendFormat": "{{controller_name}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Failures by Controller",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Rate of delete events received",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "ops"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 32
+ },
+ "id": 11,
+ "options": {
+ "legend": {
+ "calcs": ["last", "sum"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(events_delete_total{service_name=~\"$service_name\"}[5m])) by (controller_name)",
+ "legendFormat": "{{controller_name}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Delete Event Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Rate of retry attempts",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "yellow",
+ "value": 1
+ },
+ {
+ "color": "red",
+ "value": 3
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 40
+ },
+ "id": 12,
+ "options": {
+ "legend": {
+ "calcs": ["last", "max"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(reconciliations_retries_total{service_name=~\"$service_name\"}[5m])) by (controller_name)",
+ "legendFormat": "Retries - {{controller_name}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Reconciliation Retry Rate",
+ "type": "timeseries"
+ }
+ ],
+ "refresh": "10s",
+ "schemaVersion": 38,
+ "style": "dark",
+ "tags": ["operator", "kubernetes", "josdk"],
+ "templating": {
+ "list": [
+ {
+ "current": {},
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "definition": "label_values(reconciliations_started_total, service_name)",
+ "hide": 0,
+ "includeAll": false,
+ "label": "Service",
+ "multi": false,
+ "name": "service_name",
+ "options": [],
+ "query": {
+ "query": "label_values(reconciliations_started_total, service_name)",
+ "refId": "StandardVariableQuery"
+ },
+ "refresh": 2,
+ "regex": "",
+ "sort": 1,
+ "type": "query"
+ }
+ ]
+ },
+ "time": {
+ "from": "now-15m",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "JOSDK - Operator Metrics",
+ "uid": "josdk-operator-metrics",
+ "version": 0,
+ "weekStart": ""
+}
diff --git a/observability/jvm-metrics-dashboard.json b/observability/jvm-metrics-dashboard.json
new file mode 100644
index 0000000000..528f29674e
--- /dev/null
+++ b/observability/jvm-metrics-dashboard.json
@@ -0,0 +1,857 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "grafana",
+ "uid": "-- Grafana --"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": null,
+ "links": [],
+ "liveNow": false,
+ "panels": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 0
+ },
+ "id": 1,
+ "options": {
+ "legend": {
+ "calcs": ["last", "max"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "jvm_memory_used_bytes{service_name=\"josdk\"}",
+ "legendFormat": "{{area}} - {{id}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "JVM Memory Used",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 0
+ },
+ "id": 2,
+ "options": {
+ "legend": {
+ "calcs": ["last", "max"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "jvm_threads_live{service_name=\"josdk\"}",
+ "legendFormat": "Live Threads",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "jvm_threads_daemon_threads{service_name=\"josdk\"}",
+ "legendFormat": "Daemon Threads",
+ "range": true,
+ "refId": "B"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "jvm_threads_peak_threads{service_name=\"josdk\"}",
+ "legendFormat": "Peak Threads",
+ "range": true,
+ "refId": "C"
+ }
+ ],
+ "title": "JVM Threads",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 8
+ },
+ "id": 3,
+ "options": {
+ "legend": {
+ "calcs": ["last", "max"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "rate(jvm_gc_pause_milliseconds_sum{service_name=\"josdk\"}[5m])",
+ "legendFormat": "{{action}} - {{cause}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "GC Pause Time Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 8
+ },
+ "id": 4,
+ "options": {
+ "legend": {
+ "calcs": ["last"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "rate(jvm_gc_pause_milliseconds_count{service_name=\"josdk\"}[5m])",
+ "legendFormat": "{{action}} - {{cause}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "GC Pause Count Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "percentunit"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 0,
+ "y": 16
+ },
+ "id": 5,
+ "options": {
+ "orientation": "auto",
+ "reduceOptions": {
+ "values": false,
+ "calcs": ["lastNotNull"],
+ "fields": ""
+ },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "system_cpu_usage{service_name=\"josdk\"}",
+ "legendFormat": "CPU Usage",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "CPU Usage",
+ "type": "gauge"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 6,
+ "y": 16
+ },
+ "id": 6,
+ "options": {
+ "orientation": "auto",
+ "reduceOptions": {
+ "values": false,
+ "calcs": ["lastNotNull"],
+ "fields": ""
+ },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "jvm_classes_loaded{service_name=\"josdk\"}",
+ "legendFormat": "Classes Loaded",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Classes Loaded",
+ "type": "gauge"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "ms"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 12,
+ "y": 16
+ },
+ "id": 7,
+ "options": {
+ "orientation": "auto",
+ "reduceOptions": {
+ "values": false,
+ "calcs": ["lastNotNull"],
+ "fields": ""
+ },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "process_uptime_milliseconds{service_name=\"josdk\"}",
+ "legendFormat": "Uptime",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Process Uptime",
+ "type": "gauge"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 18,
+ "y": 16
+ },
+ "id": 8,
+ "options": {
+ "orientation": "auto",
+ "reduceOptions": {
+ "values": false,
+ "calcs": ["lastNotNull"],
+ "fields": ""
+ },
+ "showThresholdLabels": false,
+ "showThresholdMarkers": true
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "system_cpu_count{service_name=\"josdk\"}",
+ "legendFormat": "CPU Count",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "CPU Count",
+ "type": "gauge"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 24
+ },
+ "id": 9,
+ "options": {
+ "legend": {
+ "calcs": ["last"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "rate(jvm_gc_memory_allocated_bytes_total{service_name=\"josdk\"}[5m])",
+ "legendFormat": "Allocated",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "rate(jvm_gc_memory_promoted_bytes_total{service_name=\"josdk\"}[5m])",
+ "legendFormat": "Promoted",
+ "range": true,
+ "refId": "B"
+ }
+ ],
+ "title": "GC Memory Allocation Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 24
+ },
+ "id": 10,
+ "options": {
+ "legend": {
+ "calcs": ["last", "max"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "jvm_memory_max_bytes{service_name=\"josdk\", area=\"heap\"}",
+ "legendFormat": "Max Heap",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "jvm_memory_committed_bytes{service_name=\"josdk\", area=\"heap\"}",
+ "legendFormat": "Committed Heap",
+ "range": true,
+ "refId": "B"
+ }
+ ],
+ "title": "Heap Memory Max vs Committed",
+ "type": "timeseries"
+ }
+ ],
+ "refresh": "10s",
+ "schemaVersion": 38,
+ "style": "dark",
+ "tags": ["jvm", "java", "josdk"],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-15m",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "JOSDK - JVM Metrics",
+ "uid": "josdk-jvm-metrics",
+ "version": 0,
+ "weekStart": ""
+}
diff --git a/operator-framework-bom/pom.xml b/operator-framework-bom/pom.xml
index 04d0f9273d..ccfb2f6266 100644
--- a/operator-framework-bom/pom.xml
+++ b/operator-framework-bom/pom.xml
@@ -21,7 +21,7 @@
io.javaoperatorsdk
operator-framework-bom
- 5.2.4-SNAPSHOT
+ 999-SNAPSHOT
pom
Operator SDK - Bill of Materials
Java SDK for implementing Kubernetes operators
@@ -77,7 +77,7 @@
io.javaoperatorsdk
- operator-framework-junit-5
+ operator-framework-junit
${project.version}
diff --git a/operator-framework-core/pom.xml b/operator-framework-core/pom.xml
index aa95a5078b..2356433ca9 100644
--- a/operator-framework-core/pom.xml
+++ b/operator-framework-core/pom.xml
@@ -21,7 +21,7 @@
io.javaoperatorsdk
java-operator-sdk
- 5.2.4-SNAPSHOT
+ 999-SNAPSHOT
../pom.xml
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java
index 5adc90182d..0cfe0e997a 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/Operator.java
@@ -263,7 +263,7 @@ public RegisteredController
register(
"Cannot register reconciler with name "
+ reconciler.getClass().getCanonicalName()
+ " reconciler named "
- + ReconcilerUtils.getNameFor(reconciler)
+ + ReconcilerUtilsInternal.getNameFor(reconciler)
+ " because its configuration cannot be found.\n"
+ " Known reconcilers are: "
+ configurationService.getKnownReconcilerNames());
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternal.java
similarity index 64%
rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java
rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternal.java
index 354c2aa420..26ae5af554 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtilsInternal.java
@@ -31,10 +31,11 @@
import io.fabric8.kubernetes.client.utils.Serialization;
import io.javaoperatorsdk.operator.api.reconciler.Constants;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.NonComparableResourceVersionException;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
@SuppressWarnings("rawtypes")
-public class ReconcilerUtils {
+public class ReconcilerUtilsInternal {
private static final String FINALIZER_NAME_SUFFIX = "/finalizer";
protected static final String MISSING_GROUP_SUFFIX = ".javaoperatorsdk.io";
@@ -46,7 +47,7 @@ public class ReconcilerUtils {
Pattern.compile(".*http(s?)://[^/]*/api(s?)/(\\S*).*"); // NOSONAR: input is controlled
// prevent instantiation of util class
- private ReconcilerUtils() {}
+ private ReconcilerUtilsInternal() {}
public static boolean isFinalizerValid(String finalizer) {
return HasMetadata.validateFinalizer(finalizer);
@@ -241,4 +242,123 @@ private static boolean matchesResourceType(
}
return false;
}
+
+ /**
+ * Compares resource versions of two resources. This is a convenience method that extracts the
+ * resource versions from the metadata and delegates to {@link
+ * #validateAndCompareResourceVersions(String, String)}.
+ *
+ * @param h1 first resource
+ * @param h2 second resource
+ * @return negative if h1 is older, zero if equal, positive if h1 is newer
+ * @throws NonComparableResourceVersionException if either resource version is invalid
+ */
+ public static int validateAndCompareResourceVersions(HasMetadata h1, HasMetadata h2) {
+ return validateAndCompareResourceVersions(
+ h1.getMetadata().getResourceVersion(), h2.getMetadata().getResourceVersion());
+ }
+
+ /**
+ * Compares the resource versions of two Kubernetes resources.
+ *
+ *
This method extracts the resource versions from the metadata of both resources and delegates
+ * to {@link #compareResourceVersions(String, String)} for the actual comparison.
+ *
+ * @param h1 the first resource to compare
+ * @param h2 the second resource to compare
+ * @return a negative integer if h1's version is less than h2's version, zero if they are equal,
+ * or a positive integer if h1's version is greater than h2's version
+ * @see #compareResourceVersions(String, String)
+ */
+ public static int compareResourceVersions(HasMetadata h1, HasMetadata h2) {
+ return compareResourceVersions(
+ h1.getMetadata().getResourceVersion(), h2.getMetadata().getResourceVersion());
+ }
+
+ /**
+ * Compares two resource version strings using a length-first, then lexicographic comparison
+ * algorithm.
+ *
+ *
The comparison is performed in two steps:
+ *
+ *
+ * - First, compare the lengths of the version strings. A longer version string is considered
+ * greater than a shorter one. This works correctly for numeric versions because larger
+ * numbers have more digits (e.g., "100" > "99").
+ *
- If the lengths are equal, perform a character-by-character lexicographic comparison until
+ * a difference is found.
+ *
+ *
+ * This algorithm is more efficient than parsing the versions as numbers, especially for
+ * Kubernetes resource versions which are typically monotonically increasing numeric strings.
+ *
+ *
Note: This method does not validate that the input strings are numeric. For
+ * validated numeric comparison, use {@link #validateAndCompareResourceVersions(String, String)}.
+ *
+ * @param v1 the first resource version string
+ * @param v2 the second resource version string
+ * @return a negative integer if v1 is less than v2, zero if they are equal, or a positive integer
+ * if v1 is greater than v2
+ * @see #validateAndCompareResourceVersions(String, String)
+ */
+ public static int compareResourceVersions(String v1, String v2) {
+ int comparison = v1.length() - v2.length();
+ if (comparison != 0) {
+ return comparison;
+ }
+ for (int i = 0; i < v2.length(); i++) {
+ int comp = v1.charAt(i) - v2.charAt(i);
+ if (comp != 0) {
+ return comp;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Compares two Kubernetes resource versions numerically. Kubernetes resource versions are
+ * expected to be numeric strings that increase monotonically. This method assumes both versions
+ * are valid numeric strings without leading zeros.
+ *
+ * @param v1 first resource version
+ * @param v2 second resource version
+ * @return negative if v1 is older, zero if equal, positive if v1 is newer
+ * @throws NonComparableResourceVersionException if either resource version is empty, has leading
+ * zeros, or contains non-numeric characters
+ */
+ public static int validateAndCompareResourceVersions(String v1, String v2) {
+ int v1Length = validateResourceVersion(v1);
+ int v2Length = validateResourceVersion(v2);
+ int comparison = v1Length - v2Length;
+ if (comparison != 0) {
+ return comparison;
+ }
+ for (int i = 0; i < v2Length; i++) {
+ int comp = v1.charAt(i) - v2.charAt(i);
+ if (comp != 0) {
+ return comp;
+ }
+ }
+ return 0;
+ }
+
+ private static int validateResourceVersion(String v1) {
+ int v1Length = v1.length();
+ if (v1Length == 0) {
+ throw new NonComparableResourceVersionException("Resource version is empty");
+ }
+ for (int i = 0; i < v1Length; i++) {
+ char char1 = v1.charAt(i);
+ if (char1 == '0') {
+ if (i == 0) {
+ throw new NonComparableResourceVersionException(
+ "Resource version cannot begin with 0: " + v1);
+ }
+ } else if (char1 < '0' || char1 > '9') {
+ throw new NonComparableResourceVersionException(
+ "Non numeric characters in resource version: " + v1);
+ }
+ }
+ return v1Length;
+ }
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java
index 1a51c45b70..ba874bdc07 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/RuntimeInfo.java
@@ -63,9 +63,7 @@ private void checkIfStarted() {
public boolean allEventSourcesAreHealthy() {
checkIfStarted();
return registeredControllers.stream()
- .filter(rc -> !rc.getControllerHealthInfo().unhealthyEventSources().isEmpty())
- .findFirst()
- .isEmpty();
+ .noneMatch(rc -> rc.getControllerHealthInfo().hasUnhealthyEventSources());
}
/**
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java
index b85ee03fcb..a1b37d6fe9 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java
@@ -22,7 +22,7 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.KubernetesClient;
-import io.javaoperatorsdk.operator.ReconcilerUtils;
+import io.javaoperatorsdk.operator.ReconcilerUtilsInternal;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
/**
@@ -145,7 +145,7 @@ private String getReconcilersNameMessage() {
}
protected String keyFor(Reconciler reconciler) {
- return ReconcilerUtils.getNameFor(reconciler);
+ return ReconcilerUtilsInternal.getNameFor(reconciler);
}
@SuppressWarnings("unused")
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java
index 0a7d3ece04..6b7579b6a8 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java
@@ -28,7 +28,7 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.KubernetesClient;
-import io.javaoperatorsdk.operator.ReconcilerUtils;
+import io.javaoperatorsdk.operator.ReconcilerUtilsInternal;
import io.javaoperatorsdk.operator.api.config.Utils.Configurator;
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver;
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec;
@@ -265,7 +265,7 @@ private ResolvedControllerConfiguration
controllerCon
io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration annotation) {
final var resourceClass = getResourceClassResolver().getPrimaryResourceClass(reconcilerClass);
- final var name = ReconcilerUtils.getNameFor(reconcilerClass);
+ final var name = ReconcilerUtilsInternal.getNameFor(reconcilerClass);
final var generationAware =
valueOrDefaultFromAnnotation(
annotation,
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java
index 6215c20179..6ed9b7ff64 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java
@@ -28,8 +28,6 @@
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Secret;
-import io.fabric8.kubernetes.api.model.apps.Deployment;
-import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.CustomResource;
@@ -447,64 +445,6 @@ default Set> defaultNonSSAResource() {
return defaultNonSSAResources();
}
- /**
- * If a javaoperatorsdk.io/previous annotation should be used so that the operator sdk can detect
- * events from its own updates of dependent resources and then filter them.
- *
- * Disable this if you want to react to your own dependent resource updates
- *
- * @return if special annotation should be used for dependent resource to filter events
- * @since 4.5.0
- */
- default boolean previousAnnotationForDependentResourcesEventFiltering() {
- return true;
- }
-
- /**
- * For dependent resources, the framework can add an annotation to filter out events resulting
- * directly from the framework's operation. There are, however, some resources that do not follow
- * the Kubernetes API conventions that changes in metadata should not increase the generation of
- * the resource (as recorded in the {@code generation} field of the resource's {@code metadata}).
- * For these resources, this convention is not respected and results in a new event for the
- * framework to process. If that particular case is not handled correctly in the resource matcher,
- * the framework will consider that the resource doesn't match the desired state and therefore
- * triggers an update, which in turn, will re-add the annotation, thus starting the loop again,
- * infinitely.
- *
- *
As a workaround, we automatically skip adding previous annotation for those well-known
- * resources. Note that if you are sure that the matcher works for your use case, and it should in
- * most instances, you can remove the resource type from the blocklist.
- *
- *
The consequence of adding a resource type to the set is that the framework will not use
- * event filtering to prevent events, initiated by changes made by the framework itself as a
- * result of its processing of dependent resources, to trigger the associated reconciler again.
- *
- *
Note that this method only takes effect if annotating dependent resources to prevent
- * dependent resources events from triggering the associated reconciler again is activated as
- * controlled by {@link #previousAnnotationForDependentResourcesEventFiltering()}
- *
- * @return a Set of resource classes where the previous version annotation won't be used.
- */
- default Set> withPreviousAnnotationForDependentResourcesBlocklist() {
- return Set.of(Deployment.class, StatefulSet.class);
- }
-
- /**
- * If the event logic should parse the resourceVersion to determine the ordering of dependent
- * resource events. This is typically not needed.
- *
- * Disabled by default as Kubernetes does not support, and discourages, this interpretation of
- * resourceVersions. Enable only if your api server event processing seems to lag the operator
- * logic, and you want to further minimize the amount of work done / updates issued by the
- * operator.
- *
- * @return if resource version should be parsed (as integer)
- * @since 4.5.0
- */
- default boolean parseResourceVersionsForEventFilteringAndCaching() {
- return false;
- }
-
/**
* {@link io.javaoperatorsdk.operator.api.reconciler.UpdateControl} patch resource or status can
* either use simple patches or SSA. Setting this to {@code true}, controllers will use SSA for
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java
index 3d29bb6589..cd9cdafb39 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceOverrider.java
@@ -51,11 +51,8 @@ public class ConfigurationServiceOverrider {
private Duration reconciliationTerminationTimeout;
private Boolean ssaBasedCreateUpdateMatchForDependentResources;
private Set> defaultNonSSAResource;
- private Boolean previousAnnotationForDependentResources;
- private Boolean parseResourceVersions;
private Boolean useSSAToPatchPrimaryResource;
private Boolean cloneSecondaryResourcesWhenGettingFromCache;
- private Set> previousAnnotationUsageBlocklist;
@SuppressWarnings("rawtypes")
private DependentResourceFactory dependentResourceFactory;
@@ -168,31 +165,6 @@ public ConfigurationServiceOverrider withDefaultNonSSAResource(
return this;
}
- public ConfigurationServiceOverrider withPreviousAnnotationForDependentResources(boolean value) {
- this.previousAnnotationForDependentResources = value;
- return this;
- }
-
- /**
- * @param value true if internal algorithms can use metadata.resourceVersion as a numeric value.
- * @return this
- */
- public ConfigurationServiceOverrider withParseResourceVersions(boolean value) {
- this.parseResourceVersions = value;
- return this;
- }
-
- /**
- * @deprecated use withParseResourceVersions
- * @param value true if internal algorithms can use metadata.resourceVersion as a numeric value.
- * @return this
- */
- @Deprecated(forRemoval = true)
- public ConfigurationServiceOverrider wihtParseResourceVersions(boolean value) {
- this.parseResourceVersions = value;
- return this;
- }
-
public ConfigurationServiceOverrider withUseSSAToPatchPrimaryResource(boolean value) {
this.useSSAToPatchPrimaryResource = value;
return this;
@@ -204,12 +176,6 @@ public ConfigurationServiceOverrider withCloneSecondaryResourcesWhenGettingFromC
return this;
}
- public ConfigurationServiceOverrider withPreviousAnnotationForDependentResourcesBlocklist(
- Set> blocklist) {
- this.previousAnnotationUsageBlocklist = blocklist;
- return this;
- }
-
public ConfigurationService build() {
return new BaseConfigurationService(original.getVersion(), cloner, client) {
@Override
@@ -331,20 +297,6 @@ public Set> defaultNonSSAResources() {
defaultNonSSAResource, ConfigurationService::defaultNonSSAResources);
}
- @Override
- public boolean previousAnnotationForDependentResourcesEventFiltering() {
- return overriddenValueOrDefault(
- previousAnnotationForDependentResources,
- ConfigurationService::previousAnnotationForDependentResourcesEventFiltering);
- }
-
- @Override
- public boolean parseResourceVersionsForEventFilteringAndCaching() {
- return overriddenValueOrDefault(
- parseResourceVersions,
- ConfigurationService::parseResourceVersionsForEventFilteringAndCaching);
- }
-
@Override
public boolean useSSAToPatchPrimaryResource() {
return overriddenValueOrDefault(
@@ -357,14 +309,6 @@ public boolean cloneSecondaryResourcesWhenGettingFromCache() {
cloneSecondaryResourcesWhenGettingFromCache,
ConfigurationService::cloneSecondaryResourcesWhenGettingFromCache);
}
-
- @Override
- public Set>
- withPreviousAnnotationForDependentResourcesBlocklist() {
- return overriddenValueOrDefault(
- previousAnnotationUsageBlocklist,
- ConfigurationService::withPreviousAnnotationForDependentResourcesBlocklist);
- }
};
}
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java
index 8bddc8479e..63177b614f 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfiguration.java
@@ -20,7 +20,7 @@
import java.util.Set;
import io.fabric8.kubernetes.api.model.HasMetadata;
-import io.javaoperatorsdk.operator.ReconcilerUtils;
+import io.javaoperatorsdk.operator.ReconcilerUtilsInternal;
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec;
import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec;
import io.javaoperatorsdk.operator.api.reconciler.MaxReconciliationInterval;
@@ -42,16 +42,18 @@ default String getName() {
}
default String getFinalizerName() {
- return ReconcilerUtils.getDefaultFinalizerName(getResourceClass());
+ return ReconcilerUtilsInternal.getDefaultFinalizerName(getResourceClass());
}
static String ensureValidName(String name, String reconcilerClassName) {
- return name != null ? name : ReconcilerUtils.getDefaultReconcilerName(reconcilerClassName);
+ return name != null
+ ? name
+ : ReconcilerUtilsInternal.getDefaultReconcilerName(reconcilerClassName);
}
static String ensureValidFinalizerName(String finalizer, String resourceTypeName) {
if (finalizer != null && !finalizer.isBlank()) {
- if (ReconcilerUtils.isFinalizerValid(finalizer)) {
+ if (ReconcilerUtilsInternal.isFinalizerValid(finalizer)) {
return finalizer;
} else {
throw new IllegalArgumentException(
@@ -61,7 +63,7 @@ static String ensureValidFinalizerName(String finalizer, String resourceTypeName
+ " for details");
}
} else {
- return ReconcilerUtils.getDefaultFinalizerName(resourceTypeName);
+ return ReconcilerUtilsInternal.getDefaultFinalizerName(resourceTypeName);
}
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java
index 1072fb823d..ca777bd2cc 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfiguration.java
@@ -37,6 +37,10 @@ public class LeaderElectionConfiguration {
private final LeaderCallbacks leaderCallbacks;
private final boolean exitOnStopLeading;
+ /**
+ * @deprecated Use {@link LeaderElectionConfigurationBuilder} instead
+ */
+ @Deprecated(forRemoval = true)
public LeaderElectionConfiguration(String leaseName, String leaseNamespace, String identity) {
this(
leaseName,
@@ -49,30 +53,26 @@ public LeaderElectionConfiguration(String leaseName, String leaseNamespace, Stri
true);
}
+ /**
+ * @deprecated Use {@link LeaderElectionConfigurationBuilder} instead
+ */
+ @Deprecated(forRemoval = true)
public LeaderElectionConfiguration(String leaseName, String leaseNamespace) {
- this(
- leaseName,
- leaseNamespace,
- LEASE_DURATION_DEFAULT_VALUE,
- RENEW_DEADLINE_DEFAULT_VALUE,
- RETRY_PERIOD_DEFAULT_VALUE,
- null,
- null,
- true);
+ this(leaseName, leaseNamespace, null);
}
+ /**
+ * @deprecated Use {@link LeaderElectionConfigurationBuilder} instead
+ */
+ @Deprecated(forRemoval = true)
public LeaderElectionConfiguration(String leaseName) {
- this(
- leaseName,
- null,
- LEASE_DURATION_DEFAULT_VALUE,
- RENEW_DEADLINE_DEFAULT_VALUE,
- RETRY_PERIOD_DEFAULT_VALUE,
- null,
- null,
- true);
+ this(leaseName, null);
}
+ /**
+ * @deprecated Use {@link LeaderElectionConfigurationBuilder} instead
+ */
+ @Deprecated(forRemoval = true)
public LeaderElectionConfiguration(
String leaseName,
String leaseNamespace,
@@ -82,6 +82,10 @@ public LeaderElectionConfiguration(
this(leaseName, leaseNamespace, leaseDuration, renewDeadline, retryPeriod, null, null, true);
}
+ /**
+ * @deprecated Use {@link LeaderElectionConfigurationBuilder} instead
+ */
+ @Deprecated // this will be made package-only
public LeaderElectionConfiguration(
String leaseName,
String leaseNamespace,
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfigurationBuilder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfigurationBuilder.java
index 74f2c81cba..51ee40d84c 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfigurationBuilder.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/LeaderElectionConfigurationBuilder.java
@@ -31,7 +31,6 @@ public final class LeaderElectionConfigurationBuilder {
private Duration renewDeadline = RENEW_DEADLINE_DEFAULT_VALUE;
private Duration retryPeriod = RETRY_PERIOD_DEFAULT_VALUE;
private LeaderCallbacks leaderCallbacks;
- private boolean exitOnStopLeading = true;
private LeaderElectionConfigurationBuilder(String leaseName) {
this.leaseName = leaseName;
@@ -71,12 +70,22 @@ public LeaderElectionConfigurationBuilder withLeaderCallbacks(LeaderCallbacks le
return this;
}
+ /**
+ * @deprecated Use {@link #buildForTest(boolean)} instead as setting this to false should only be
+ * used for testing purposes
+ */
+ @Deprecated(forRemoval = true)
public LeaderElectionConfigurationBuilder withExitOnStopLeading(boolean exitOnStopLeading) {
- this.exitOnStopLeading = exitOnStopLeading;
- return this;
+ throw new UnsupportedOperationException(
+ "Setting exitOnStopLeading should only be used for testing purposes, use buildForTest"
+ + " instead");
}
public LeaderElectionConfiguration build() {
+ return buildForTest(false);
+ }
+
+ public LeaderElectionConfiguration buildForTest(boolean exitOnStopLeading) {
return new LeaderElectionConfiguration(
leaseName,
leaseNamespace,
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java
index 9264db66bc..e6655641a2 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/Informer.java
@@ -28,6 +28,7 @@
import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter;
import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter;
+import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION;
import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES;
import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_LONG_VALUE_SET;
import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_VALUE_SET;
@@ -131,4 +132,11 @@
/** Kubernetes field selector for additional resource filtering */
Field[] fieldSelector() default {};
+
+ /**
+ * true if we can consider resource versions as integers, therefore it is valid to compare them
+ *
+ * @since 5.3.0
+ */
+ boolean comparableResourceVersions() default DEFAULT_COMPARABLE_RESOURCE_VERSION;
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java
index 24f78eb7be..f6caa4fe4d 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerConfiguration.java
@@ -25,7 +25,7 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.informers.cache.ItemStore;
import io.javaoperatorsdk.operator.OperatorException;
-import io.javaoperatorsdk.operator.ReconcilerUtils;
+import io.javaoperatorsdk.operator.ReconcilerUtilsInternal;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.config.Utils;
import io.javaoperatorsdk.operator.api.reconciler.Constants;
@@ -53,6 +53,7 @@ public class InformerConfiguration {
private ItemStore itemStore;
private Long informerListLimit;
private FieldSelector fieldSelector;
+ private boolean comparableResourceVersions;
protected InformerConfiguration(
Class resourceClass,
@@ -66,7 +67,8 @@ protected InformerConfiguration(
GenericFilter super R> genericFilter,
ItemStore itemStore,
Long informerListLimit,
- FieldSelector fieldSelector) {
+ FieldSelector fieldSelector,
+ boolean comparableResourceVersions) {
this(resourceClass);
this.name = name;
this.namespaces = namespaces;
@@ -79,6 +81,7 @@ protected InformerConfiguration(
this.itemStore = itemStore;
this.informerListLimit = informerListLimit;
this.fieldSelector = fieldSelector;
+ this.comparableResourceVersions = comparableResourceVersions;
}
private InformerConfiguration(Class resourceClass) {
@@ -89,7 +92,7 @@ private InformerConfiguration(Class resourceClass) {
// controller
// where GenericKubernetesResource now does not apply
? GenericKubernetesResource.class.getSimpleName()
- : ReconcilerUtils.getResourceTypeName(resourceClass);
+ : ReconcilerUtilsInternal.getResourceTypeName(resourceClass);
}
@SuppressWarnings({"rawtypes", "unchecked"})
@@ -113,7 +116,8 @@ public static InformerConfiguration.Builder builder(
original.genericFilter,
original.itemStore,
original.informerListLimit,
- original.fieldSelector)
+ original.fieldSelector,
+ original.comparableResourceVersions)
.builder;
}
@@ -288,6 +292,10 @@ public FieldSelector getFieldSelector() {
return fieldSelector;
}
+ public boolean isComparableResourceVersions() {
+ return comparableResourceVersions;
+ }
+
@SuppressWarnings("UnusedReturnValue")
public class Builder {
@@ -359,6 +367,7 @@ public InformerConfiguration.Builder initFromAnnotation(
Arrays.stream(informerConfig.fieldSelector())
.map(f -> new FieldSelector.Field(f.path(), f.value(), f.negated()))
.toList()));
+ withComparableResourceVersions(informerConfig.comparableResourceVersions());
}
return this;
}
@@ -459,5 +468,10 @@ public Builder withFieldSelector(FieldSelector fieldSelector) {
InformerConfiguration.this.fieldSelector = fieldSelector;
return this;
}
+
+ public Builder withComparableResourceVersions(boolean comparableResourceVersions) {
+ InformerConfiguration.this.comparableResourceVersions = comparableResourceVersions;
+ return this;
+ }
}
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java
index bca605a41c..69903e805f 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/informer/InformerEventSourceConfiguration.java
@@ -33,6 +33,7 @@
import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter;
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
+import static io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_COMPARABLE_RESOURCE_VERSION;
import static io.javaoperatorsdk.operator.api.reconciler.Constants.SAME_AS_CONTROLLER_NAMESPACES_SET;
import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_ALL_NAMESPACE_SET;
import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE_SET;
@@ -96,18 +97,21 @@ class DefaultInformerEventSourceConfiguration
private final GroupVersionKind groupVersionKind;
private final InformerConfiguration informerConfig;
private final KubernetesClient kubernetesClient;
+ private final boolean comparableResourceVersion;
protected DefaultInformerEventSourceConfiguration(
GroupVersionKind groupVersionKind,
PrimaryToSecondaryMapper> primaryToSecondaryMapper,
SecondaryToPrimaryMapper secondaryToPrimaryMapper,
InformerConfiguration informerConfig,
- KubernetesClient kubernetesClient) {
+ KubernetesClient kubernetesClient,
+ boolean comparableResourceVersion) {
this.informerConfig = Objects.requireNonNull(informerConfig);
this.groupVersionKind = groupVersionKind;
this.primaryToSecondaryMapper = primaryToSecondaryMapper;
this.secondaryToPrimaryMapper = secondaryToPrimaryMapper;
this.kubernetesClient = kubernetesClient;
+ this.comparableResourceVersion = comparableResourceVersion;
}
@Override
@@ -135,6 +139,11 @@ public Optional getGroupVersionKind() {
public Optional getKubernetesClient() {
return Optional.ofNullable(kubernetesClient);
}
+
+ @Override
+ public boolean comparableResourceVersion() {
+ return this.comparableResourceVersion;
+ }
}
@SuppressWarnings({"unused", "UnusedReturnValue"})
@@ -148,6 +157,7 @@ class Builder {
private PrimaryToSecondaryMapper> primaryToSecondaryMapper;
private SecondaryToPrimaryMapper secondaryToPrimaryMapper;
private KubernetesClient kubernetesClient;
+ private boolean comparableResourceVersion = DEFAULT_COMPARABLE_RESOURCE_VERSION;
private Builder(Class resourceClass, Class extends HasMetadata> primaryResourceClass) {
this(resourceClass, primaryResourceClass, null);
@@ -285,6 +295,11 @@ public Builder withFieldSelector(FieldSelector fieldSelector) {
return this;
}
+ public Builder withComparableResourceVersion(boolean comparableResourceVersion) {
+ this.comparableResourceVersion = comparableResourceVersion;
+ return this;
+ }
+
public void updateFrom(InformerConfiguration informerConfig) {
if (informerConfig != null) {
final var informerConfigName = informerConfig.getName();
@@ -324,7 +339,10 @@ public InformerEventSourceConfiguration build() {
HasMetadata.getKind(primaryResourceClass),
false)),
config.build(),
- kubernetesClient);
+ kubernetesClient,
+ comparableResourceVersion);
}
}
+
+ boolean comparableResourceVersion();
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/AggregatedMetrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/AggregatedMetrics.java
index f66bdc47c6..396014cacc 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/AggregatedMetrics.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/AggregatedMetrics.java
@@ -83,8 +83,9 @@ public void reconcileCustomResource(
@Override
public void failedReconciliation(
- HasMetadata resource, Exception exception, Map metadata) {
- metricsList.forEach(metrics -> metrics.failedReconciliation(resource, exception, metadata));
+ HasMetadata resource, RetryInfo retry, Exception exception, Map metadata) {
+ metricsList.forEach(
+ metrics -> metrics.failedReconciliation(resource, retry, exception, metadata));
}
@Override
@@ -93,8 +94,10 @@ public void reconciliationExecutionStarted(HasMetadata resource, Map metadata) {
- metricsList.forEach(metrics -> metrics.reconciliationExecutionFinished(resource, metadata));
+ public void reconciliationExecutionFinished(
+ HasMetadata resource, RetryInfo retryInfo, Map metadata) {
+ metricsList.forEach(
+ metrics -> metrics.reconciliationExecutionFinished(resource, retryInfo, metadata));
}
@Override
@@ -103,8 +106,9 @@ public void cleanupDoneFor(ResourceID resourceID, Map metadata)
}
@Override
- public void finishedReconciliation(HasMetadata resource, Map metadata) {
- metricsList.forEach(metrics -> metrics.finishedReconciliation(resource, metadata));
+ public void successfullyFinishedReconciliation(
+ HasMetadata resource, Map metadata) {
+ metricsList.forEach(metrics -> metrics.successfullyFinishedReconciliation(resource, metadata));
}
@Override
@@ -113,6 +117,7 @@ public T timeControllerExecution(ControllerExecution execution) throws Ex
}
@Override
+ @Deprecated(forRemoval = true)
public > T monitorSizeOf(T map, String name) {
metricsList.forEach(metrics -> metrics.monitorSizeOf(map, name));
return map;
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java
index 10b2db6774..1f4981c226 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/monitoring/Metrics.java
@@ -23,6 +23,7 @@
import io.javaoperatorsdk.operator.processing.Controller;
import io.javaoperatorsdk.operator.processing.event.Event;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.retry.RetryExecution;
/**
* An interface that metrics providers can implement and that the SDK will call at different times
@@ -50,30 +51,67 @@ default void controllerRegistered(Controller extends HasMetadata> controller)
default void receivedEvent(Event event, Map metadata) {}
/**
- * Called right before a resource is dispatched to the ExecutorService for reconciliation.
- *
+ * @deprecated use {@link Metrics#submittedForReconciliation(HasMetadata, RetryInfo, Map)} Called
+ * right before a resource is dispatched to the ExecutorService for reconciliation.
* @param resource the associated with the resource
* @param retryInfo the current retry state information for the reconciliation request
* @param metadata metadata associated with the resource being processed
*/
+ @Deprecated(forRemoval = true)
default void reconcileCustomResource(
+ HasMetadata resource, RetryInfo retryInfo, Map metadata) {
+ submittedForReconciliation(resource, retryInfo, metadata);
+ }
+
+ /**
+ * Called right before a resource is submitted to the ExecutorService for reconciliation.
+ *
+ * @param resource the associated with the resource
+ * @param retryInfo the current retry state information for the reconciliation request
+ * @param metadata metadata associated with the resource being processed
+ */
+ default void submittedForReconciliation(
HasMetadata resource, RetryInfo retryInfo, Map metadata) {}
+ default void reconciliationExecutionStarted(HasMetadata resource, Map metadata) {}
+
/**
* Called when a precedent reconciliation for the resource associated with the specified {@link
* ResourceID} resulted in the provided exception, resulting in a retry of the reconciliation.
*
* @param resource the {@link ResourceID} associated with the resource being processed
+ * @param retryInfo the state of retry before {@link RetryExecution#nextDelay()} is called
* @param exception the exception that caused the failed reconciliation resulting in a retry
* @param metadata metadata associated with the resource being processed
*/
default void failedReconciliation(
- HasMetadata resource, Exception exception, Map metadata) {}
+ HasMetadata resource,
+ RetryInfo retryInfo,
+ Exception exception,
+ Map metadata) {}
- default void reconciliationExecutionStarted(HasMetadata resource, Map metadata) {}
+ /**
+ * Called when the {@link
+ * io.javaoperatorsdk.operator.api.reconciler.Reconciler#reconcile(HasMetadata, Context)} method
+ * of the Reconciler associated with the resource associated with the specified {@link ResourceID}
+ * has successfully finished.
+ *
+ * @param resource the {@link ResourceID} associated with the resource being processed
+ * @param metadata metadata associated with the resource being processed
+ */
+ default void successfullyFinishedReconciliation(
+ HasMetadata resource, Map metadata) {}
+ /**
+ * Always called not only if successfully finished.
+ *
+ * @param resource the {@link ResourceID} associated with the resource being processed
+ * @param retryInfo not that this retry info in state after {@link RetryExecution#nextDelay()} is
+ * called in case of exception.
+ * @param metadata metadata associated with the resource being processed
+ */
default void reconciliationExecutionFinished(
- HasMetadata resource, Map metadata) {}
+ HasMetadata resource, RetryInfo retryInfo, Map metadata) {}
/**
* Called when the resource associated with the specified {@link ResourceID} has been successfully
@@ -85,15 +123,14 @@ default void reconciliationExecutionFinished(
default void cleanupDoneFor(ResourceID resourceID, Map metadata) {}
/**
- * Called when the {@link
- * io.javaoperatorsdk.operator.api.reconciler.Reconciler#reconcile(HasMetadata, Context)} method
- * of the Reconciler associated with the resource associated with the specified {@link ResourceID}
- * has sucessfully finished.
- *
+ * @deprecated use {@link Metrics#successfullyFinishedReconciliation(HasMetadata, Map)}
* @param resource the {@link ResourceID} associated with the resource being processed
* @param metadata metadata associated with the resource being processed
*/
- default void finishedReconciliation(HasMetadata resource, Map metadata) {}
+ @Deprecated(forRemoval = true)
+ default void finishedReconciliation(HasMetadata resource, Map metadata) {
+ successfullyFinishedReconciliation(resource, metadata);
+ }
/**
* Encapsulates the information about a controller execution i.e. a call to either {@link
@@ -185,6 +222,7 @@ default T timeControllerExecution(ControllerExecution execution) throws E
* @param the type of the Map being monitored
*/
@SuppressWarnings("unused")
+ @Deprecated(forRemoval = true)
default > T monitorSizeOf(T map, String name) {
return map;
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java
index 5087f4052a..6ac46ee0a6 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/BaseControl.java
@@ -21,22 +21,53 @@
public abstract class BaseControl> {
+ public static final Long INSTANT_RESCHEDULE = 0L;
+
private Long scheduleDelay = null;
+ /**
+ * Schedules a reconciliation to occur after the specified delay in milliseconds.
+ *
+ * @param delay the delay in milliseconds after which to reschedule
+ * @return this control instance for fluent chaining
+ */
public T rescheduleAfter(long delay) {
rescheduleAfter(Duration.ofMillis(delay));
return (T) this;
}
+ /**
+ * Schedules a reconciliation to occur after the specified delay.
+ *
+ * @param delay the {@link Duration} after which to reschedule
+ * @return this control instance for fluent chaining
+ */
public T rescheduleAfter(Duration delay) {
this.scheduleDelay = delay.toMillis();
return (T) this;
}
+ /**
+ * Schedules a reconciliation to occur after the specified delay using the given time unit.
+ *
+ * @param delay the delay value
+ * @param timeUnit the time unit of the delay
+ * @return this control instance for fluent chaining
+ */
public T rescheduleAfter(long delay, TimeUnit timeUnit) {
return rescheduleAfter(timeUnit.toMillis(delay));
}
+ /**
+ * Schedules an instant reconciliation. The reconciliation will be triggered as soon as possible.
+ *
+ * @return this control instance for fluent chaining
+ */
+ public T reschedule() {
+ this.scheduleDelay = INSTANT_RESCHEDULE;
+ return (T) this;
+ }
+
public Optional getScheduleDelay() {
return Optional.ofNullable(scheduleDelay);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java
index 052b4d8c44..7330a407c1 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Constants.java
@@ -41,6 +41,7 @@ public final class Constants {
public static final String RESOURCE_GVK_KEY = "josdk.resource.gvk";
public static final String CONTROLLER_NAME = "controller.name";
public static final boolean DEFAULT_FOLLOW_CONTROLLER_NAMESPACE_CHANGES = true;
+ public static final boolean DEFAULT_COMPARABLE_RESOURCE_VERSION = true;
private Constants() {}
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java
index cc7c865dc5..2df74d4298 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java
@@ -35,12 +35,83 @@ default Optional getSecondaryResource(Class expectedType) {
return getSecondaryResource(expectedType, null);
}
- Set getSecondaryResources(Class expectedType);
+ /**
+ * Retrieves a {@link Set} of the secondary resources of the specified type, which are associated
+ * with the primary resource being processed, possibly making sure that only the latest version of
+ * each resource is retrieved.
+ *
+ * Note: While this method returns a {@link Set}, it is possible to get several copies of a
+ * given resource albeit all with different {@code resourceVersion}. If you want to avoid this
+ * situation, call {@link #getSecondaryResources(Class, boolean)} with the {@code deduplicate}
+ * parameter set to {@code true}.
+ *
+ * @param expectedType a class representing the type of secondary resources to retrieve
+ * @param the type of secondary resources to retrieve
+ * @return a {@link Stream} of secondary resources of the specified type, possibly deduplicated
+ */
+ default Set getSecondaryResources(Class expectedType) {
+ return getSecondaryResources(expectedType, false);
+ }
+
+ /**
+ * Retrieves a {@link Set} of the secondary resources of the specified type, which are associated
+ * with the primary resource being processed, possibly making sure that only the latest version of
+ * each resource is retrieved.
+ *
+ * Note: While this method returns a {@link Set}, it is possible to get several copies of a
+ * given resource albeit all with different {@code resourceVersion}. If you want to avoid this
+ * situation, ask for the deduplicated version by setting the {@code deduplicate} parameter to
+ * {@code true}.
+ *
+ * @param expectedType a class representing the type of secondary resources to retrieve
+ * @param deduplicate {@code true} if only the latest version of each resource should be kept,
+ * {@code false} otherwise
+ * @param the type of secondary resources to retrieve
+ * @return a {@link Set} of secondary resources of the specified type, possibly deduplicated
+ * @throws IllegalArgumentException if the secondary resource type cannot be deduplicated because
+ * it's not extending {@link HasMetadata}, which is required to access the resource version
+ * @since 5.3.0
+ */
+ Set getSecondaryResources(Class expectedType, boolean deduplicate);
+ /**
+ * Retrieves a {@link Stream} of the secondary resources of the specified type, which are
+ * associated with the primary resource being processed, possibly making sure that only the latest
+ * version of each resource is retrieved.
+ *
+ * Note: It is possible to get several copies of a given resource albeit all with different
+ * {@code resourceVersion}. If you want to avoid this situation, call {@link
+ * #getSecondaryResourcesAsStream(Class, boolean)} with the {@code deduplicate} parameter set to
+ * {@code true}.
+ *
+ * @param expectedType a class representing the type of secondary resources to retrieve
+ * @param the type of secondary resources to retrieve
+ * @return a {@link Stream} of secondary resources of the specified type, possibly deduplicated
+ */
default Stream getSecondaryResourcesAsStream(Class expectedType) {
- return getSecondaryResources(expectedType).stream();
+ return getSecondaryResourcesAsStream(expectedType, false);
}
+ /**
+ * Retrieves a {@link Stream} of the secondary resources of the specified type, which are
+ * associated with the primary resource being processed, possibly making sure that only the latest
+ * version of each resource is retrieved.
+ *
+ * Note: It is possible to get several copies of a given resource albeit all with different
+ * {@code resourceVersion}. If you want to avoid this situation, ask for the deduplicated version
+ * by setting the {@code deduplicate} parameter to {@code true}.
+ *
+ * @param expectedType a class representing the type of secondary resources to retrieve
+ * @param deduplicate {@code true} if only the latest version of each resource should be kept,
+ * {@code false} otherwise
+ * @param the type of secondary resources to retrieve
+ * @return a {@link Stream} of secondary resources of the specified type, possibly deduplicated
+ * @throws IllegalArgumentException if the secondary resource type cannot be deduplicated because
+ * it's not extending {@link HasMetadata}, which is required to access the resource version
+ * @since 5.3.0
+ */
+ Stream getSecondaryResourcesAsStream(Class expectedType, boolean deduplicate);
+
Optional getSecondaryResource(Class expectedType, String eventSourceName);
ControllerConfiguration getControllerConfiguration();
@@ -58,6 +129,8 @@ default Stream getSecondaryResourcesAsStream(Class expectedType) {
KubernetesClient getClient();
+ ResourceOperations resourceOperations();
+
/** ExecutorService initialized by framework for workflows. Used for workflow standalone mode. */
ExecutorService getWorkflowExecutorService();
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java
index f3fade4659..ac5a7b41b9 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java
@@ -15,15 +15,21 @@
*/
package io.javaoperatorsdk.operator.api.reconciler;
+import java.util.HashSet;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
+import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.ReconcilerUtilsInternal;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedWorkflowAndDependentResourceContext;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext;
import io.javaoperatorsdk.operator.processing.Controller;
@@ -32,7 +38,6 @@
import io.javaoperatorsdk.operator.processing.event.ResourceID;
public class DefaultContext
implements Context
{
-
private RetryInfo retryInfo;
private final Controller
controller;
private final P primaryResource;
@@ -41,6 +46,8 @@ public class DefaultContext
implements Context
{
defaultManagedDependentResourceContext;
private final boolean primaryResourceDeleted;
private final boolean primaryResourceFinalStateUnknown;
+ private final Map, Object> desiredStates = new ConcurrentHashMap<>();
+ private final ResourceOperations resourceOperations;
public DefaultContext(
RetryInfo retryInfo,
@@ -56,6 +63,7 @@ public DefaultContext(
this.primaryResourceFinalStateUnknown = primaryResourceFinalStateUnknown;
this.defaultManagedDependentResourceContext =
new DefaultManagedWorkflowAndDependentResourceContext<>(controller, primaryResource, this);
+ this.resourceOperations = new ResourceOperations<>(this);
}
@Override
@@ -64,15 +72,44 @@ public Optional getRetryInfo() {
}
@Override
- public Set getSecondaryResources(Class expectedType) {
+ public Set getSecondaryResources(Class expectedType, boolean deduplicate) {
+ if (deduplicate) {
+ final var deduplicatedMap = deduplicatedMap(getSecondaryResourcesAsStream(expectedType));
+ return new HashSet<>(deduplicatedMap.values());
+ }
return getSecondaryResourcesAsStream(expectedType).collect(Collectors.toSet());
}
- @Override
- public Stream getSecondaryResourcesAsStream(Class expectedType) {
- return controller.getEventSourceManager().getEventSourcesFor(expectedType).stream()
- .map(es -> es.getSecondaryResources(primaryResource))
- .flatMap(Set::stream);
+ public Stream getSecondaryResourcesAsStream(Class expectedType, boolean deduplicate) {
+ final var stream =
+ controller.getEventSourceManager().getEventSourcesFor(expectedType).stream()
+ .mapMulti(
+ (es, consumer) -> es.getSecondaryResources(primaryResource).forEach(consumer));
+ if (deduplicate) {
+ if (!HasMetadata.class.isAssignableFrom(expectedType)) {
+ throw new IllegalArgumentException("Can only de-duplicate HasMetadata descendants");
+ }
+ return deduplicatedMap(stream).values().stream();
+ } else {
+ return stream;
+ }
+ }
+
+ private Map deduplicatedMap(Stream stream) {
+ return stream.collect(
+ Collectors.toUnmodifiableMap(
+ DefaultContext::resourceID,
+ Function.identity(),
+ (existing, replacement) ->
+ compareResourceVersions(existing, replacement) >= 0 ? existing : replacement));
+ }
+
+ private static ResourceID resourceID(Object hasMetadata) {
+ return ResourceID.fromResource((HasMetadata) hasMetadata);
+ }
+
+ private static int compareResourceVersions(Object v1, Object v2) {
+ return ReconcilerUtilsInternal.compareResourceVersions((HasMetadata) v1, (HasMetadata) v2);
}
@Override
@@ -119,6 +156,11 @@ public KubernetesClient getClient() {
return controller.getClient();
}
+ @Override
+ public ResourceOperations resourceOperations() {
+ return resourceOperations;
+ }
+
@Override
public ExecutorService getWorkflowExecutorService() {
// note that this should be always received from executor service manager, so we are able to do
@@ -157,4 +199,12 @@ public DefaultContext
setRetryInfo(RetryInfo retryInfo) {
this.retryInfo = retryInfo;
return this;
}
+
+ @SuppressWarnings("unchecked")
+ public R getOrComputeDesiredStateFor(
+ DependentResource dependentResource, Function desiredStateComputer) {
+ return (R)
+ desiredStates.computeIfAbsent(
+ dependentResource, ignored -> desiredStateComputer.apply(getPrimaryResource()));
+ }
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java
index 6103b4b12b..f74cd49ee7 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java
@@ -45,7 +45,11 @@
* caches the updated resource from the response in an overlay cache on top of the Informer cache.
* If the update fails, it reads the primary resource from the cluster, applies the modifications
* again and retries the update.
+ *
+ * @deprecated Use {@link Context#resourceOperations()} that contains the more efficient up-to-date
+ * versions of methods.
*/
+@Deprecated(forRemoval = true)
public class PrimaryUpdateAndCacheUtils {
public static final int DEFAULT_MAX_RETRY = 10;
@@ -450,4 +454,45 @@ public static
P addFinalizerWithSSA(
e);
}
}
+
+ public static int compareResourceVersions(HasMetadata h1, HasMetadata h2) {
+ return compareResourceVersions(
+ h1.getMetadata().getResourceVersion(), h2.getMetadata().getResourceVersion());
+ }
+
+ public static int compareResourceVersions(String v1, String v2) {
+ int v1Length = validateResourceVersion(v1);
+ int v2Length = validateResourceVersion(v2);
+ int comparison = v1Length - v2Length;
+ if (comparison != 0) {
+ return comparison;
+ }
+ for (int i = 0; i < v2Length; i++) {
+ int comp = v1.charAt(i) - v2.charAt(i);
+ if (comp != 0) {
+ return comp;
+ }
+ }
+ return 0;
+ }
+
+ private static int validateResourceVersion(String v1) {
+ int v1Length = v1.length();
+ if (v1Length == 0) {
+ throw new NonComparableResourceVersionException("Resource version is empty");
+ }
+ for (int i = 0; i < v1Length; i++) {
+ char char1 = v1.charAt(i);
+ if (char1 == '0') {
+ if (i == 0) {
+ throw new NonComparableResourceVersionException(
+ "Resource version cannot begin with 0: " + v1);
+ }
+ } else if (char1 < '0' || char1 > '9') {
+ throw new NonComparableResourceVersionException(
+ "Non numeric characters in resource version: " + v1);
+ }
+ }
+ return v1Length;
+ }
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java
new file mode 100644
index 0000000000..de4d00d717
--- /dev/null
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceOperations.java
@@ -0,0 +1,756 @@
+/*
+ * 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.
+ */
+package io.javaoperatorsdk.operator.api.reconciler;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.client.KubernetesClientException;
+import io.fabric8.kubernetes.client.dsl.base.PatchContext;
+import io.fabric8.kubernetes.client.dsl.base.PatchType;
+import io.javaoperatorsdk.operator.OperatorException;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource;
+
+import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getUID;
+import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getVersion;
+
+/**
+ * Provides useful operations to manipulate resources (server-side apply, patch, etc.) in an
+ * idiomatic way, in particular to make sure that the latest version of the resource is present in
+ * the caches for the next reconciliation.
+ *
+ * @param
the resource type on which this object operates
+ */
+public class ResourceOperations
{
+
+ public static final int DEFAULT_MAX_RETRY = 10;
+
+ private static final Logger log = LoggerFactory.getLogger(ResourceOperations.class);
+
+ private final Context
context;
+
+ public ResourceOperations(Context
context) {
+ this.context = context;
+ }
+
+ /**
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from the update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata. In case of SSA we advise not to do updates with optimistic locking.
+ *
+ * @param resource fresh resource for server side apply
+ * @return updated resource
+ * @param resource type
+ */
+ public R serverSideApply(R resource) {
+ return resourcePatch(
+ resource,
+ r ->
+ context
+ .getClient()
+ .resource(r)
+ .patch(
+ new PatchContext.Builder()
+ .withForce(true)
+ .withFieldManager(context.getControllerConfiguration().fieldManager())
+ .withPatchType(PatchType.SERVER_SIDE_APPLY)
+ .build()));
+ }
+
+ /**
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from the update, so
+ * reconciliation is not triggered by own update.
+ *
+ * You are free to control the optimistic locking by setting the resource version in resource
+ * metadata. In case of SSA we advise not to do updates with optimistic locking.
+ *
+ * @param resource fresh resource for server side apply
+ * @return updated resource
+ * @param informerEventSource InformerEventSource to use for resource caching and filtering
+ * @param resource type
+ */
+ public R serverSideApply(
+ R resource, InformerEventSource informerEventSource) {
+ if (informerEventSource == null) {
+ return serverSideApply(resource);
+ }
+ return resourcePatch(
+ resource,
+ r ->
+ context
+ .getClient()
+ .resource(r)
+ .patch(
+ new PatchContext.Builder()
+ .withForce(true)
+ .withFieldManager(context.getControllerConfiguration().fieldManager())
+ .withPatchType(PatchType.SERVER_SIDE_APPLY)
+ .build()),
+ informerEventSource);
+ }
+
+ /**
+ * Server-Side Apply the resource status subresource.
+ *
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata. In case of SSA we advise not to do updates with optimistic locking.
+ *
+ * @param resource fresh resource for server side apply
+ * @return updated resource
+ * @param resource type
+ */
+ public R serverSideApplyStatus(R resource) {
+ return resourcePatch(
+ resource,
+ r ->
+ context
+ .getClient()
+ .resource(r)
+ .subresource("status")
+ .patch(
+ new PatchContext.Builder()
+ .withForce(true)
+ .withFieldManager(context.getControllerConfiguration().fieldManager())
+ .withPatchType(PatchType.SERVER_SIDE_APPLY)
+ .build()));
+ }
+
+ /**
+ * Server-Side Apply the primary resource.
+ *
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata. In case of SSA we advise not to do updates with optimistic locking.
+ *
+ * @param resource primary resource for server side apply
+ * @return updated resource
+ */
+ public P serverSideApplyPrimary(P resource) {
+ return resourcePatch(
+ resource,
+ r ->
+ context
+ .getClient()
+ .resource(r)
+ .patch(
+ new PatchContext.Builder()
+ .withForce(true)
+ .withFieldManager(context.getControllerConfiguration().fieldManager())
+ .withPatchType(PatchType.SERVER_SIDE_APPLY)
+ .build()),
+ context.eventSourceRetriever().getControllerEventSource());
+ }
+
+ /**
+ * Server-Side Apply the primary resource status subresource.
+ *
+ *
Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata. In case of SSA we advise not to do updates with optimistic locking.
+ *
+ * @param resource primary resource for server side apply
+ * @return updated resource
+ */
+ public P serverSideApplyPrimaryStatus(P resource) {
+ return resourcePatch(
+ resource,
+ r ->
+ context
+ .getClient()
+ .resource(r)
+ .subresource("status")
+ .patch(
+ new PatchContext.Builder()
+ .withForce(true)
+ .withFieldManager(context.getControllerConfiguration().fieldManager())
+ .withPatchType(PatchType.SERVER_SIDE_APPLY)
+ .build()),
+ context.eventSourceRetriever().getControllerEventSource());
+ }
+
+ /**
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource resource to update
+ * @return updated resource
+ * @param resource type
+ */
+ public R update(R resource) {
+ return resourcePatch(resource, r -> context.getClient().resource(r).update());
+ }
+
+ /**
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ * You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource resource to update
+ * @return updated resource
+ * @param informerEventSource InformerEventSource to use for resource caching and filtering
+ * @param resource type
+ */
+ public R update(
+ R resource, InformerEventSource informerEventSource) {
+ if (informerEventSource == null) {
+ return update(resource);
+ }
+ return resourcePatch(
+ resource, r -> context.getClient().resource(r).update(), informerEventSource);
+ }
+
+ /**
+ * Creates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ * You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource resource to update
+ * @return updated resource
+ * @param resource type
+ */
+ public R create(R resource) {
+ return resourcePatch(resource, r -> context.getClient().resource(r).create());
+ }
+
+ /**
+ * Creates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ * You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource resource to update
+ * @return updated resource
+ * @param informerEventSource InformerEventSource to use for resource caching and filtering
+ * @param resource type
+ */
+ public R create(
+ R resource, InformerEventSource informerEventSource) {
+ if (informerEventSource == null) {
+ return create(resource);
+ }
+ return resourcePatch(
+ resource, r -> context.getClient().resource(r).create(), informerEventSource);
+ }
+
+ /**
+ * Updates the resource status subresource.
+ *
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource resource to update
+ * @return updated resource
+ * @param resource type
+ */
+ public R updateStatus(R resource) {
+ return resourcePatch(resource, r -> context.getClient().resource(r).updateStatus());
+ }
+
+ /**
+ * Updates the primary resource.
+ *
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource primary resource to update
+ * @return updated resource
+ */
+ public P updatePrimary(P resource) {
+ return resourcePatch(
+ resource,
+ r -> context.getClient().resource(r).update(),
+ context.eventSourceRetriever().getControllerEventSource());
+ }
+
+ /**
+ * Updates the primary resource status subresource.
+ *
+ *
Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource primary resource to update
+ * @return updated resource
+ */
+ public P updatePrimaryStatus(P resource) {
+ return resourcePatch(
+ resource,
+ r -> context.getClient().resource(r).updateStatus(),
+ context.eventSourceRetriever().getControllerEventSource());
+ }
+
+ /**
+ * Applies a JSON Patch to the resource. The unaryOperator function is used to modify the
+ * resource, and the differences are sent as a JSON Patch to the Kubernetes API server.
+ *
+ *
Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource resource to patch
+ * @param unaryOperator function to modify the resource
+ * @return updated resource
+ * @param resource type
+ */
+ public R jsonPatch(R resource, UnaryOperator unaryOperator) {
+ return resourcePatch(resource, r -> context.getClient().resource(r).edit(unaryOperator));
+ }
+
+ /**
+ * Applies a JSON Patch to the resource status subresource. The unaryOperator function is used to
+ * modify the resource status, and the differences are sent as a JSON Patch.
+ *
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource resource to patch
+ * @param unaryOperator function to modify the resource
+ * @return updated resource
+ * @param resource type
+ */
+ public R jsonPatchStatus(R resource, UnaryOperator unaryOperator) {
+ return resourcePatch(resource, r -> context.getClient().resource(r).editStatus(unaryOperator));
+ }
+
+ /**
+ * Applies a JSON Patch to the primary resource.
+ *
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource primary resource to patch
+ * @param unaryOperator function to modify the resource
+ * @return updated resource
+ */
+ public P jsonPatchPrimary(P resource, UnaryOperator
unaryOperator) {
+ return resourcePatch(
+ resource,
+ r -> context.getClient().resource(r).edit(unaryOperator),
+ context.eventSourceRetriever().getControllerEventSource());
+ }
+
+ /**
+ * Applies a JSON Patch to the primary resource status subresource.
+ *
+ *
Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource primary resource to patch
+ * @param unaryOperator function to modify the resource
+ * @return updated resource
+ */
+ public P jsonPatchPrimaryStatus(P resource, UnaryOperator
unaryOperator) {
+ return resourcePatch(
+ resource,
+ r -> context.getClient().resource(r).editStatus(unaryOperator),
+ context.eventSourceRetriever().getControllerEventSource());
+ }
+
+ /**
+ * Applies a JSON Merge Patch to the resource. JSON Merge Patch (RFC 7386) is a simpler patching
+ * strategy that merges the provided resource with the existing resource on the server.
+ *
+ *
Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource resource to patch
+ * @return updated resource
+ * @param resource type
+ */
+ public R jsonMergePatch(R resource) {
+ return resourcePatch(resource, r -> context.getClient().resource(r).patch());
+ }
+
+ /**
+ * Applies a JSON Merge Patch to the resource status subresource.
+ *
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource resource to patch
+ * @return updated resource
+ * @param resource type
+ */
+ public R jsonMergePatchStatus(R resource) {
+ return resourcePatch(resource, r -> context.getClient().resource(r).patchStatus());
+ }
+
+ /**
+ * Applies a JSON Merge Patch to the primary resource. Caches the response using the controller's
+ * event source.
+ *
+ * Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource primary resource to patch reconciliation
+ * @return updated resource
+ */
+ public P jsonMergePatchPrimary(P resource) {
+ return resourcePatch(
+ resource,
+ r -> context.getClient().resource(r).patch(),
+ context.eventSourceRetriever().getControllerEventSource());
+ }
+
+ /**
+ * Applies a JSON Merge Patch to the primary resource.
+ *
+ *
Updates the resource and caches the response if needed, thus making sure that next
+ * reconciliation will see to updated resource - or more recent one if additional update happened
+ * after this update; In addition to that it filters out the event from this update, so
+ * reconciliation is not triggered by own update.
+ *
+ *
You are free to control the optimistic locking by setting the resource version in resource
+ * metadata.
+ *
+ * @param resource primary resource to patch
+ * @return updated resource
+ * @see #jsonMergePatchPrimaryStatus(HasMetadata)
+ */
+ public P jsonMergePatchPrimaryStatus(P resource) {
+ return resourcePatch(
+ resource,
+ r -> context.getClient().resource(r).patchStatus(),
+ context.eventSourceRetriever().getControllerEventSource());
+ }
+
+ /**
+ * Utility method to patch a resource and cache the result. Automatically discovers the event
+ * source for the resource type and delegates to {@link #resourcePatch(HasMetadata, UnaryOperator,
+ * ManagedInformerEventSource)}.
+ *
+ * @param resource resource to patch
+ * @param updateOperation operation to perform (update, patch, edit, etc.)
+ * @return updated resource
+ * @param resource type
+ * @throws IllegalStateException if no event source or multiple event sources are found
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public R resourcePatch(R resource, UnaryOperator updateOperation) {
+
+ var esList = context.eventSourceRetriever().getEventSourcesFor(resource.getClass());
+ if (esList.isEmpty()) {
+ throw new IllegalStateException("No event source found for type: " + resource.getClass());
+ }
+ var es = esList.get(0);
+ if (esList.size() > 1) {
+ log.warn(
+ "Multiple event sources found for type: {}, selecting first with name {}",
+ resource.getClass(),
+ es.name());
+ }
+ if (es instanceof ManagedInformerEventSource mes) {
+ return resourcePatch(resource, updateOperation, (ManagedInformerEventSource) mes);
+ } else {
+ throw new IllegalStateException(
+ "Target event source must be a subclass off "
+ + ManagedInformerEventSource.class.getName());
+ }
+ }
+
+ /**
+ * Utility method to patch a resource and cache the result using the specified event source. This
+ * method either filters out the resulting event or allows it to trigger reconciliation based on
+ * the filterEvent parameter.
+ *
+ * @param resource resource to patch
+ * @param updateOperation operation to perform (update, patch, edit, etc.)
+ * @param ies the managed informer event source to use for caching
+ * @return updated resource
+ * @param resource type
+ */
+ public R resourcePatch(
+ R resource, UnaryOperator updateOperation, ManagedInformerEventSource ies) {
+ return ies.eventFilteringUpdateAndCacheResource(resource, updateOperation);
+ }
+
+ /**
+ * Adds the default finalizer (from controller configuration) to the primary resource. This is a
+ * convenience method that calls {@link #addFinalizer(String)} with the configured finalizer name.
+ * Note that explicitly adding/removing finalizer is required only if "Trigger reconciliation on
+ * all event" mode is on.
+ *
+ * @return updated resource from the server response
+ * @see #addFinalizer(String)
+ */
+ public P addFinalizer() {
+ return addFinalizer(context.getControllerConfiguration().getFinalizerName());
+ }
+
+ /**
+ * Adds finalizer to the resource using JSON Patch. Retries conflicts and unprocessable content
+ * (HTTP 422). It does not try to add finalizer if there is already a finalizer or resource is
+ * marked for deletion. Note that explicitly adding/removing finalizer is required only if
+ * "Trigger reconciliation on all event" mode is on.
+ *
+ * @return updated resource from the server response
+ */
+ public P addFinalizer(String finalizerName) {
+ var resource = context.getPrimaryResource();
+ if (resource.isMarkedForDeletion() || resource.hasFinalizer(finalizerName)) {
+ return resource;
+ }
+ return conflictRetryingPatchPrimary(
+ r -> {
+ r.addFinalizer(finalizerName);
+ return r;
+ },
+ r -> !r.hasFinalizer(finalizerName));
+ }
+
+ /**
+ * Removes the default finalizer (from controller configuration) from the primary resource. This
+ * is a convenience method that calls {@link #removeFinalizer(String)} with the configured
+ * finalizer name. Note that explicitly adding/removing finalizer is required only if "Trigger
+ * reconciliation on all event" mode is on.
+ *
+ * @return updated resource from the server response
+ * @see #removeFinalizer(String)
+ */
+ public P removeFinalizer() {
+ return removeFinalizer(context.getControllerConfiguration().getFinalizerName());
+ }
+
+ /**
+ * Removes the target finalizer from the primary resource. Uses JSON Patch and handles retries. It
+ * does not try to remove finalizer if finalizer is not present on the resource. Note that
+ * explicitly adding/removing finalizer is required only if "Trigger reconciliation on all event"
+ * mode is on.
+ *
+ * @return updated resource from the server response
+ */
+ public P removeFinalizer(String finalizerName) {
+ var resource = context.getPrimaryResource();
+ if (!resource.hasFinalizer(finalizerName)) {
+ return resource;
+ }
+ return conflictRetryingPatchPrimary(
+ r -> {
+ r.removeFinalizer(finalizerName);
+ return r;
+ },
+ r -> {
+ if (r == null) {
+ log.warn("Cannot remove finalizer since resource not exists.");
+ return false;
+ }
+ return r.hasFinalizer(finalizerName);
+ });
+ }
+
+ /**
+ * Patches the resource using JSON Patch. In case the server responds with conflict (HTTP 409) or
+ * unprocessable content (HTTP 422) it retries the operation up to the maximum number defined in
+ * {@link ResourceOperations#DEFAULT_MAX_RETRY}.
+ *
+ * @param resourceChangesOperator changes to be done on the resource before update
+ * @param preCondition condition to check if the patch operation still needs to be performed or
+ * not.
+ * @return updated resource from the server or unchanged if the precondition does not hold.
+ */
+ @SuppressWarnings("unchecked")
+ public P conflictRetryingPatchPrimary(
+ UnaryOperator resourceChangesOperator, Predicate
preCondition) {
+ var resource = context.getPrimaryResource();
+ var client = context.getClient();
+ if (log.isDebugEnabled()) {
+ log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource));
+ }
+ int retryIndex = 0;
+ while (true) {
+ try {
+ if (!preCondition.test(resource)) {
+ return resource;
+ }
+ return jsonPatchPrimary(resource, resourceChangesOperator);
+ } catch (KubernetesClientException e) {
+ log.trace("Exception during patch for resource: {}", resource);
+ retryIndex++;
+ // only retry on conflict (409) and unprocessable content (422) which
+ // can happen if JSON Patch is not a valid request since there was
+ // a concurrent request which already removed another finalizer:
+ // List element removal from a list is by index in JSON Patch
+ // so if addressing a second finalizer but first is meanwhile removed
+ // it is a wrong request.
+ if (e.getCode() != 409 && e.getCode() != 422) {
+ throw e;
+ }
+ if (retryIndex >= DEFAULT_MAX_RETRY) {
+ throw new OperatorException(
+ "Exceeded maximum ("
+ + DEFAULT_MAX_RETRY
+ + ") retry attempts to patch resource: "
+ + ResourceID.fromResource(resource));
+ }
+ log.debug(
+ "Retrying patch for resource name: {}, namespace: {}; HTTP code: {}",
+ resource.getMetadata().getName(),
+ resource.getMetadata().getNamespace(),
+ e.getCode());
+ var operation = client.resources(resource.getClass());
+ if (resource.getMetadata().getNamespace() != null) {
+ resource =
+ (P)
+ operation
+ .inNamespace(resource.getMetadata().getNamespace())
+ .withName(resource.getMetadata().getName())
+ .get();
+ } else {
+ resource = (P) operation.withName(resource.getMetadata().getName()).get();
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds the default finalizer (from controller configuration) to the primary resource using
+ * Server-Side Apply. This is a convenience method that calls {@link #addFinalizerWithSSA(
+ * String)} with the configured finalizer name. Note that explicitly adding finalizer is required
+ * only if "Trigger reconciliation on all event" mode is on.
+ *
+ * @return the patched resource from the server response
+ * @see #addFinalizerWithSSA(String)
+ */
+ public P addFinalizerWithSSA() {
+ return addFinalizerWithSSA(context.getControllerConfiguration().getFinalizerName());
+ }
+
+ /**
+ * Adds finalizer using Server-Side Apply. In the background this method creates a fresh copy of
+ * the target resource, setting only name, namespace and finalizer. Does not use optimistic
+ * locking for the patch. Note that explicitly adding finalizer is required only if "Trigger
+ * reconciliation on all event" mode is on.
+ *
+ * @param finalizerName name of the finalizer to add
+ * @return the patched resource from the server response
+ */
+ public P addFinalizerWithSSA(String finalizerName) {
+ var originalResource = context.getPrimaryResource();
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "Adding finalizer (using SSA) for resource: {} version: {}",
+ getUID(originalResource),
+ getVersion(originalResource));
+ }
+ try {
+ @SuppressWarnings("unchecked")
+ P resource = (P) originalResource.getClass().getConstructor().newInstance();
+ resource.initNameAndNamespaceFrom(originalResource);
+ resource.addFinalizer(finalizerName);
+
+ return serverSideApplyPrimary(resource);
+ } catch (InstantiationException
+ | IllegalAccessException
+ | InvocationTargetException
+ | NoSuchMethodException e) {
+ throw new RuntimeException(
+ "Issue with creating custom resource instance with reflection."
+ + " Custom Resources must provide a no-arg constructor. Class: "
+ + originalResource.getClass().getName(),
+ e);
+ }
+ }
+}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java
index 4a78e60f05..f2a9359e04 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/ControllerHealthInfo.java
@@ -16,7 +16,10 @@
package io.javaoperatorsdk.operator.health;
import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collector;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import io.javaoperatorsdk.operator.processing.event.EventSourceManager;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
@@ -25,6 +28,17 @@
@SuppressWarnings("rawtypes")
public class ControllerHealthInfo {
+ private static final Predicate UNHEALTHY = e -> e.getStatus() == Status.UNHEALTHY;
+ private static final Predicate INFORMER =
+ e -> e instanceof InformerWrappingEventSourceHealthIndicator;
+ private static final Predicate UNHEALTHY_INFORMER =
+ e -> INFORMER.test(e) && e.getStatus() == Status.UNHEALTHY;
+ private static final Collector>
+ NAME_TO_ES_MAP = Collectors.toMap(EventSource::name, e -> e);
+ private static final Collector<
+ EventSource, ?, Map>
+ NAME_TO_ES_HEALTH_MAP =
+ Collectors.toMap(EventSource::name, e -> (InformerWrappingEventSourceHealthIndicator) e);
private final EventSourceManager> eventSourceManager;
public ControllerHealthInfo(EventSourceManager eventSourceManager) {
@@ -32,23 +46,31 @@ public ControllerHealthInfo(EventSourceManager eventSourceManager) {
}
public Map eventSourceHealthIndicators() {
- return eventSourceManager.allEventSources().stream()
- .collect(Collectors.toMap(EventSource::name, e -> e));
+ return eventSourceManager.allEventSourcesStream().collect(NAME_TO_ES_MAP);
+ }
+
+ /**
+ * Whether the associated {@link io.javaoperatorsdk.operator.processing.Controller} has unhealthy
+ * event sources.
+ *
+ * @return {@code true} if any of the associated controller is unhealthy, {@code false} otherwise
+ * @since 5.3.0
+ */
+ public boolean hasUnhealthyEventSources() {
+ return filteredEventSources(UNHEALTHY).findAny().isPresent();
}
public Map unhealthyEventSources() {
- return eventSourceManager.allEventSources().stream()
- .filter(e -> e.getStatus() == Status.UNHEALTHY)
- .collect(Collectors.toMap(EventSource::name, e -> e));
+ return filteredEventSources(UNHEALTHY).collect(NAME_TO_ES_MAP);
+ }
+
+ private Stream filteredEventSources(Predicate filter) {
+ return eventSourceManager.allEventSourcesStream().filter(filter);
}
public Map
informerEventSourceHealthIndicators() {
- return eventSourceManager.allEventSources().stream()
- .filter(e -> e instanceof InformerWrappingEventSourceHealthIndicator)
- .collect(
- Collectors.toMap(
- EventSource::name, e -> (InformerWrappingEventSourceHealthIndicator) e));
+ return filteredEventSources(INFORMER).collect(NAME_TO_ES_HEALTH_MAP);
}
/**
@@ -58,11 +80,6 @@ public Map unhealthyEventSources() {
*/
public Map
unhealthyInformerEventSourceHealthIndicators() {
- return eventSourceManager.allEventSources().stream()
- .filter(e -> e.getStatus() == Status.UNHEALTHY)
- .filter(e -> e instanceof InformerWrappingEventSourceHealthIndicator)
- .collect(
- Collectors.toMap(
- EventSource::name, e -> (InformerWrappingEventSourceHealthIndicator) e));
+ return filteredEventSources(UNHEALTHY_INFORMER).collect(NAME_TO_ES_HEALTH_MAP);
}
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java
index 66d24aa383..6c39a2601b 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/health/InformerHealthIndicator.java
@@ -23,8 +23,5 @@ public interface InformerHealthIndicator extends EventSourceHealthIndicator {
boolean isRunning();
- @Override
- Status getStatus();
-
String getTargetNamespace();
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java
index 01a8b62e9d..716490388d 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/MDCUtils.java
@@ -20,8 +20,10 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.config.Utils;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.ResourceAction;
public class MDCUtils {
+ public static final String NO_NAMESPACE = "no namespace";
private static final String NAME = "resource.name";
private static final String NAMESPACE = "resource.namespace";
@@ -30,10 +32,40 @@ public class MDCUtils {
private static final String RESOURCE_VERSION = "resource.resourceVersion";
private static final String GENERATION = "resource.generation";
private static final String UID = "resource.uid";
- private static final String NO_NAMESPACE = "no namespace";
private static final boolean enabled =
Utils.getBooleanFromSystemPropsOrDefault(Utils.USE_MDC_ENV_KEY, true);
+ private static final String EVENT_SOURCE_PREFIX = "eventsource.event.";
+ private static final String EVENT_ACTION = EVENT_SOURCE_PREFIX + "action";
+ private static final String EVENT_SOURCE_NAME = "eventsource.name";
+
+ public static void addInformerEventInfo(
+ HasMetadata resource, ResourceAction action, String eventSourceName) {
+ if (enabled) {
+ addResourceInfo(resource, true);
+ MDC.put(EVENT_ACTION, action.name());
+ MDC.put(EVENT_SOURCE_NAME, eventSourceName);
+ }
+ }
+
+ public static void removeInformerEventInfo() {
+ if (enabled) {
+ removeResourceInfo(true);
+ MDC.remove(EVENT_ACTION);
+ MDC.remove(EVENT_SOURCE_NAME);
+ }
+ }
+
+ public static void withMDCForEvent(
+ HasMetadata resource, ResourceAction action, Runnable runnable, String eventSourceName) {
+ try {
+ MDCUtils.addInformerEventInfo(resource, action, eventSourceName);
+ runnable.run();
+ } finally {
+ MDCUtils.removeInformerEventInfo();
+ }
+ }
+
public static void addResourceIDInfo(ResourceID resourceID) {
if (enabled) {
MDC.put(NAME, resourceID.getName());
@@ -49,33 +81,46 @@ public static void removeResourceIDInfo() {
}
public static void addResourceInfo(HasMetadata resource) {
+ addResourceInfo(resource, false);
+ }
+
+ public static void addResourceInfo(HasMetadata resource, boolean forEventSource) {
if (enabled) {
- MDC.put(API_VERSION, resource.getApiVersion());
- MDC.put(KIND, resource.getKind());
+ MDC.put(key(API_VERSION, forEventSource), resource.getApiVersion());
+ MDC.put(key(KIND, forEventSource), resource.getKind());
final var metadata = resource.getMetadata();
if (metadata != null) {
- MDC.put(NAME, metadata.getName());
- if (metadata.getNamespace() != null) {
- MDC.put(NAMESPACE, metadata.getNamespace());
- }
- MDC.put(RESOURCE_VERSION, metadata.getResourceVersion());
+ MDC.put(key(NAME, forEventSource), metadata.getName());
+
+ final var namespace = metadata.getNamespace();
+ MDC.put(key(NAMESPACE, forEventSource), namespace != null ? namespace : NO_NAMESPACE);
+
+ MDC.put(key(RESOURCE_VERSION, forEventSource), metadata.getResourceVersion());
if (metadata.getGeneration() != null) {
- MDC.put(GENERATION, metadata.getGeneration().toString());
+ MDC.put(key(GENERATION, forEventSource), metadata.getGeneration().toString());
}
- MDC.put(UID, metadata.getUid());
+ MDC.put(key(UID, forEventSource), metadata.getUid());
}
}
}
+ private static String key(String baseKey, boolean forEventSource) {
+ return forEventSource ? EVENT_SOURCE_PREFIX + baseKey : baseKey;
+ }
+
public static void removeResourceInfo() {
+ removeResourceInfo(false);
+ }
+
+ public static void removeResourceInfo(boolean forEventSource) {
if (enabled) {
- MDC.remove(API_VERSION);
- MDC.remove(KIND);
- MDC.remove(NAME);
- MDC.remove(NAMESPACE);
- MDC.remove(RESOURCE_VERSION);
- MDC.remove(GENERATION);
- MDC.remove(UID);
+ MDC.remove(key(API_VERSION, forEventSource));
+ MDC.remove(key(KIND, forEventSource));
+ MDC.remove(key(NAME, forEventSource));
+ MDC.remove(key(NAMESPACE, forEventSource));
+ MDC.remove(key(RESOURCE_VERSION, forEventSource));
+ MDC.remove(key(GENERATION, forEventSource));
+ MDC.remove(key(UID, forEventSource));
}
}
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java
index a7c5ce9e2d..8dc62b4ca7 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java
@@ -23,6 +23,7 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.DefaultContext;
import io.javaoperatorsdk.operator.api.reconciler.Ignore;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
@@ -85,7 +86,7 @@ protected ReconcileResult reconcile(P primary, R actualResource, Context c
if (creatable() || updatable()) {
if (actualResource == null) {
if (creatable) {
- var desired = desired(primary, context);
+ var desired = getOrComputeDesired(context);
throwIfNull(desired, primary, "Desired");
logForOperation("Creating", primary, desired);
var createdResource = handleCreate(desired, primary, context);
@@ -95,7 +96,8 @@ protected ReconcileResult reconcile(P primary, R actualResource, Context c
if (updatable()) {
final Matcher.Result match = match(actualResource, primary, context);
if (!match.matched()) {
- final var desired = match.computedDesired().orElseGet(() -> desired(primary, context));
+ final var desired =
+ match.computedDesired().orElseGet(() -> getOrComputeDesired(context));
throwIfNull(desired, primary, "Desired");
logForOperation("Updating", primary, desired);
var updatedResource = handleUpdate(actualResource, desired, primary, context);
@@ -127,7 +129,6 @@ protected ReconcileResult reconcile(P primary, R actualResource, Context c
@Override
public Optional getSecondaryResource(P primary, Context context) {
-
var secondaryResources = context.getSecondaryResources(resourceType());
if (secondaryResources.isEmpty()) {
return Optional.empty();
@@ -212,6 +213,27 @@ protected R desired(P primary, Context
context) {
+ " updated");
}
+ /**
+ * Retrieves the desired state from the {@link Context} if it has already been computed or calls
+ * {@link #desired(HasMetadata, Context)} and stores its result in the context for further use.
+ * This ensures that {@code desired} is only called once per reconciliation to avoid unneeded
+ * processing and supports scenarios where idempotent computation of the desired state is not
+ * feasible.
+ *
+ *
Note that this method should normally only be called by the SDK itself and exclusively (i.e.
+ * {@link #desired(HasMetadata, Context)} should not be called directly by the SDK) whenever the
+ * desired state is needed to ensure it is properly cached for the current reconciliation.
+ *
+ * @param context the {@link Context} in scope for the current reconciliation
+ * @return the desired state associated with this dependent resource based on the currently
+ * in-scope primary resource as found in the context
+ */
+ protected R getOrComputeDesired(Context
context) {
+ assert context instanceof DefaultContext
;
+ DefaultContext
defaultContext = (DefaultContext
) context;
+ return defaultContext.getOrComputeDesiredStateFor(this, p -> desired(p, defaultContext));
+ }
+
public void delete(P primary, Context
context) {
dependentResourceReconciler.delete(primary, context);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java
index e601e937cf..7b83a377c1 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java
@@ -105,7 +105,7 @@ protected void handleExplicitStateCreation(P primary, R created, Context
cont
@Override
public Matcher.Result match(R resource, P primary, Context context) {
- var desired = desired(primary, context);
+ var desired = getOrComputeDesired(context);
return Matcher.Result.computed(resource.equals(desired), desired);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java
index 5b3617c26c..23135f81b1 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java
@@ -27,7 +27,6 @@
import io.javaoperatorsdk.operator.api.reconciler.Ignore;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
-import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result;
class BulkDependentResourceReconciler
implements DependentResourceReconciler {
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java
index 0ba48797af..5562c883e2 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java
@@ -138,7 +138,7 @@ public static Matcher.Result m
Context context,
boolean labelsAndAnnotationsEquality,
String... ignorePaths) {
- final var desired = dependentResource.desired(primary, context);
+ final var desired = dependentResource.getOrComputeDesired(context);
return match(desired, actualResource, labelsAndAnnotationsEquality, context, ignorePaths);
}
@@ -150,7 +150,7 @@ public static Matcher.Result m
boolean specEquality,
boolean labelsAndAnnotationsEquality,
String... ignorePaths) {
- final var desired = dependentResource.desired(primary, context);
+ final var desired = dependentResource.getOrComputeDesired(context);
return match(
desired, actualResource, labelsAndAnnotationsEquality, specEquality, context, ignorePaths);
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GroupVersionKindPlural.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GroupVersionKindPlural.java
index 4818760888..569526f4e3 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GroupVersionKindPlural.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GroupVersionKindPlural.java
@@ -119,7 +119,7 @@ public static GroupVersionKindPlural gvkFor(Class extends HasMetadata> resourc
* @return the default plural form for the specified kind
*/
public static String getDefaultPluralFor(String kind) {
- // todo: replace by Fabric8 version when available, see
+ // replace by Fabric8 version when available, see
// https://github.com/fabric8io/kubernetes-client/pull/6314
return kind != null ? Pluralize.toPlural(kind.toLowerCase()) : null;
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java
index 05cddcade1..f8d7c07b01 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java
@@ -25,7 +25,6 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Namespaced;
-import io.fabric8.kubernetes.client.dsl.Resource;
import io.javaoperatorsdk.operator.api.config.dependent.Configured;
import io.javaoperatorsdk.operator.api.config.informer.InformerEventSourceConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Context;
@@ -55,7 +54,6 @@ public abstract class KubernetesDependentResource kubernetesDependentResourceConfig;
private volatile Boolean useSSA;
- private volatile Boolean usePreviousAnnotationForEventFiltering;
public KubernetesDependentResource() {}
@@ -74,7 +72,8 @@ public void configureWith(KubernetesDependentResourceConfig config) {
@SuppressWarnings("unused")
public R create(R desired, P primary, Context context) {
- if (useSSA(context)) {
+ var ssa = useSSA(context);
+ if (ssa) {
// setting resource version for SSA so only created if it doesn't exist already
var createIfNotExisting =
kubernetesDependentResourceConfig == null
@@ -86,35 +85,40 @@ public R create(R desired, P primary, Context
context) {
}
}
addMetadata(false, null, desired, primary, context);
- final var resource = prepare(context, desired, primary, "Creating");
- return useSSA(context)
- ? resource
- .fieldManager(context.getControllerConfiguration().fieldManager())
- .forceConflicts()
- .serverSideApply()
- : resource.create();
+ log.debug(
+ "Creating target resource with type: {}, with id: {} use ssa: {}",
+ desired.getClass(),
+ ResourceID.fromResource(desired),
+ ssa);
+
+ return ssa
+ ? context.resourceOperations().serverSideApply(desired, eventSource().orElse(null))
+ : context.resourceOperations().create(desired, eventSource().orElse(null));
}
public R update(R actual, R desired, P primary, Context
context) {
- boolean useSSA = useSSA(context);
+ boolean ssa = useSSA(context);
if (log.isDebugEnabled()) {
log.debug(
"Updating actual resource: {} version: {}; SSA: {}",
ResourceID.fromResource(actual),
actual.getMetadata().getResourceVersion(),
- useSSA);
+ ssa);
}
R updatedResource;
addMetadata(false, actual, desired, primary, context);
- if (useSSA) {
+ log.debug(
+ "Updating target resource with type: {}, with id: {} use ssa: {}",
+ desired.getClass(),
+ ResourceID.fromResource(desired),
+ ssa);
+ if (ssa) {
updatedResource =
- prepare(context, desired, primary, "Updating")
- .fieldManager(context.getControllerConfiguration().fieldManager())
- .forceConflicts()
- .serverSideApply();
+ context.resourceOperations().serverSideApply(desired, eventSource().orElse(null));
} else {
var updatedActual = GenericResourceUpdater.updateResource(actual, desired, context);
- updatedResource = prepare(context, updatedActual, primary, "Updating").update();
+ updatedResource =
+ context.resourceOperations().update(updatedActual, eventSource().orElse(null));
}
log.debug(
"Resource version after update: {}", updatedResource.getMetadata().getResourceVersion());
@@ -123,7 +127,7 @@ public R update(R actual, R desired, P primary, Context
context) {
@Override
public Result match(R actualResource, P primary, Context context) {
- final var desired = desired(primary, context);
+ final var desired = getOrComputeDesired(context);
return match(actualResource, desired, primary, context);
}
@@ -158,14 +162,6 @@ protected void addMetadata(
} else {
annotations.remove(InformerEventSource.PREVIOUS_ANNOTATION_KEY);
}
- } else if (usePreviousAnnotation(context)) { // set a new one
- eventSource()
- .orElseThrow()
- .addPreviousAnnotation(
- Optional.ofNullable(actualResource)
- .map(r -> r.getMetadata().getResourceVersion())
- .orElse(null),
- target);
}
addReferenceHandlingMetadata(target, primary);
}
@@ -181,22 +177,6 @@ protected boolean useSSA(Context
context) {
return useSSA;
}
- private boolean usePreviousAnnotation(Context
context) {
- if (usePreviousAnnotationForEventFiltering == null) {
- usePreviousAnnotationForEventFiltering =
- context
- .getControllerConfiguration()
- .getConfigurationService()
- .previousAnnotationForDependentResourcesEventFiltering()
- && !context
- .getControllerConfiguration()
- .getConfigurationService()
- .withPreviousAnnotationForDependentResourcesBlocklist()
- .contains(this.resourceType());
- }
- return usePreviousAnnotationForEventFiltering;
- }
-
@Override
protected void handleDelete(P primary, R secondary, Context
context) {
if (secondary != null) {
@@ -209,17 +189,6 @@ public void deleteTargetResource(P primary, R resource, ResourceID key, Context<
context.getClient().resource(resource).delete();
}
- @SuppressWarnings("unused")
- protected Resource prepare(Context context, R desired, P primary, String actionName) {
- log.debug(
- "{} target resource with type: {}, with id: {}",
- actionName,
- desired.getClass(),
- ResourceID.fromResource(desired));
-
- return context.getClient().resource(desired);
- }
-
protected void addReferenceHandlingMetadata(R desired, P primary) {
if (addOwnerReference()) {
desired.addOwnerReference(primary);
@@ -301,7 +270,7 @@ protected Optional selectTargetSecondaryResource(
* @return id of the target managed resource
*/
protected ResourceID targetSecondaryResourceID(P primary, Context context) {
- return ResourceID.fromResource(desired(primary, context));
+ return ResourceID.fromResource(getOrComputeDesired(context));
}
protected boolean addOwnerReference() {
@@ -309,8 +278,8 @@ protected boolean addOwnerReference() {
}
@Override
- protected R desired(P primary, Context
context) {
- return super.desired(primary, context);
+ protected R getOrComputeDesired(Context
context) {
+ return super.getOrComputeDesired(context);
}
@Override
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java
index 3685b509aa..e4c1d54161 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventProcessor.java
@@ -37,15 +37,13 @@
import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter;
import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter.RateLimitState;
import io.javaoperatorsdk.operator.processing.event.source.Cache;
-import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction;
+import io.javaoperatorsdk.operator.processing.event.source.ResourceAction;
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceDeleteEvent;
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEvent;
import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource;
import io.javaoperatorsdk.operator.processing.retry.Retry;
import io.javaoperatorsdk.operator.processing.retry.RetryExecution;
-import static io.javaoperatorsdk.operator.processing.KubernetesResourceUtils.getName;
-
public class EventProcessor
implements EventHandler, LifecycleAware {
private static final Logger log = LoggerFactory.getLogger(EventProcessor.class);
@@ -187,9 +185,8 @@ private void submitReconciliationExecution(ResourceState state) {
executor.execute(new ReconcilerExecutor(resourceID, executionScope));
} else {
log.debug(
- "Skipping executing controller for resource id: {}. Controller in execution: {}. Latest"
+ "Skipping executing controller. Controller in execution: {}. Latest"
+ " Resource present: {}",
- resourceID,
controllerUnderExecution,
maybeLatest.isPresent());
if (maybeLatest.isEmpty()) {
@@ -198,7 +195,7 @@ private void submitReconciliationExecution(ResourceState state) {
// resource. Other is that simply there is no primary resource present for an event, this
// might indicate issue with the implementation, but could happen also naturally, thus
// this is not necessarily a problem.
- log.debug("no primary resource found in cache with resource id: {}", resourceID);
+ log.debug("No primary resource found in cache with resource id: {}", resourceID);
}
}
} finally {
@@ -209,7 +206,7 @@ private void submitReconciliationExecution(ResourceState state) {
@SuppressWarnings("unchecked")
private P getResourceFromState(ResourceState state) {
if (triggerOnAllEvents()) {
- log.debug("Getting resource from state for {}", state.getId());
+ log.debug("Getting resource from state");
return (P) state.getLastKnownResource();
} else {
throw new IllegalStateException(
@@ -218,10 +215,9 @@ private P getResourceFromState(ResourceState state) {
}
private void handleEventMarking(Event event, ResourceState state) {
- final var relatedCustomResourceID = event.getRelatedCustomResourceID();
if (event instanceof ResourceEvent resourceEvent) {
if (resourceEvent.getAction() == ResourceAction.DELETED) {
- log.debug("Marking delete event received for: {}", relatedCustomResourceID);
+ log.debug("Marking delete event received");
state.markDeleteEventReceived(
resourceEvent.getResource().orElseThrow(),
((ResourceDeleteEvent) resourceEvent).isDeletedFinalStateUnknown());
@@ -229,8 +225,7 @@ private void handleEventMarking(Event event, ResourceState state) {
if (state.processedMarkForDeletionPresent() && isResourceMarkedForDeletion(resourceEvent)) {
log.debug(
"Skipping mark of event received, since already processed mark for deletion and"
- + " resource marked for deletion: {}",
- relatedCustomResourceID);
+ + " resource marked for deletion");
return;
}
// Normally when eventMarker is in state PROCESSED_MARK_FOR_DELETION it is expected to
@@ -260,8 +255,7 @@ private boolean isResourceMarkedForDeletion(ResourceEvent resourceEvent) {
private void handleRateLimitedSubmission(ResourceID resourceID, Duration minimalDuration) {
var minimalDurationMillis = minimalDuration.toMillis();
- log.debug(
- "Rate limited resource: {}, rescheduled in {} millis", resourceID, minimalDurationMillis);
+ log.debug("Rate limited resource; rescheduled in {} millis", minimalDurationMillis);
retryEventSource()
.scheduleOnce(
resourceID, Math.max(minimalDurationMillis, MINIMAL_RATE_LIMIT_RESCHEDULE_DURATION));
@@ -298,7 +292,6 @@ synchronized void eventProcessingFinished(
cleanupForDeletedEvent(executionScope.getResourceID());
} else if (postExecutionControl.isFinalizerRemoved()) {
state.markProcessedMarkForDeletion();
- metrics.cleanupDoneFor(resourceID, metricsMetadata);
} else {
if (state.eventPresent() || isTriggerOnAllEventAndDeleteEventPresent(state)) {
log.debug("Submitting for reconciliation.");
@@ -334,7 +327,7 @@ private void reScheduleExecutionIfInstructed(
.ifPresentOrElse(
delay -> {
var resourceID = ResourceID.fromResource(customResource);
- log.debug("Rescheduling event for resource: {} with delay: {}", resourceID, delay);
+ log.debug("Rescheduling event with delay: {}", delay);
retryEventSource().scheduleOnce(resourceID, delay);
},
() -> scheduleExecutionForMaxReconciliationInterval(customResource));
@@ -347,11 +340,7 @@ private void scheduleExecutionForMaxReconciliationInterval(P customResource) {
m -> {
var resourceID = ResourceID.fromResource(customResource);
var delay = m.toMillis();
- log.debug(
- "Rescheduling event for max reconciliation interval for resource: {} : "
- + "with delay: {}",
- resourceID,
- delay);
+ log.debug("Rescheduling event for max reconciliation interval with delay: {}", delay);
retryEventSource().scheduleOnce(resourceID, delay);
});
}
@@ -372,20 +361,18 @@ private void handleRetryOnException(ExecutionScope
executionScope, Exception
state.eventPresent()
|| (triggerOnAllEvents() && state.isAdditionalEventPresentAfterDeleteEvent());
state.markEventReceived(triggerOnAllEvents());
-
retryAwareErrorLogging(state.getRetry(), eventPresent, exception, executionScope);
+ metrics.failedReconciliation(
+ executionScope.getResource(), state.getRetry(), exception, metricsMetadata);
if (eventPresent) {
- log.debug("New events exists for for resource id: {}", resourceID);
+ log.debug("New events exist for resource id");
submitReconciliationExecution(state);
return;
}
Optional nextDelay = state.getRetry().nextDelay();
-
nextDelay.ifPresentOrElse(
delay -> {
- log.debug(
- "Scheduling timer event for retry with delay:{} for resource: {}", delay, resourceID);
- metrics.failedReconciliation(executionScope.getResource(), exception, metricsMetadata);
+ log.debug("Scheduling timer event for retry with delay:{}", delay);
retryEventSource().scheduleOnce(resourceID, delay);
},
() -> {
@@ -425,8 +412,7 @@ private void retryAwareErrorLogging(
}
private void cleanupOnSuccessfulExecution(ExecutionScope executionScope) {
- log.debug(
- "Cleanup for successful execution for resource: {}", getName(executionScope.getResource()));
+ log.debug("Cleanup for successful execution");
if (isRetryConfigured()) {
resourceStateManager.getOrCreate(executionScope.getResourceID()).setRetry(null);
}
@@ -444,7 +430,7 @@ private ResourceState getOrInitRetryExecution(ExecutionScope
executionScope)
}
private void cleanupForDeletedEvent(ResourceID resourceID) {
- log.debug("Cleaning up for delete event for: {}", resourceID);
+ log.debug("Cleaning up for delete event");
resourceStateManager.remove(resourceID);
metrics.cleanupDoneFor(resourceID, metricsMetadata);
}
@@ -509,6 +495,7 @@ public void run() {
log.debug("Event processor not running skipping resource processing: {}", resourceID);
return;
}
+ MDCUtils.addResourceIDInfo(resourceID);
log.debug("Running reconcile executor for: {}", executionScope);
// change thread name for easier debugging
final var thread = Thread.currentThread();
@@ -518,9 +505,7 @@ public void run() {
var actualResource = cache.get(resourceID);
if (actualResource.isEmpty()) {
if (triggerOnAllEvents()) {
- log.debug(
- "Resource not found in the cache, checking for delete event resource: {}",
- resourceID);
+ log.debug("Resource not found in the cache, checking for delete event resource");
if (executionScope.isDeleteEvent()) {
var state = resourceStateManager.get(resourceID);
actualResource =
@@ -538,7 +523,7 @@ public void run() {
return;
}
} else {
- log.debug("Skipping execution; primary resource missing from cache: {}", resourceID);
+ log.debug("Skipping execution; primary resource missing from cache");
return;
}
}
@@ -550,7 +535,8 @@ public void run() {
reconciliationDispatcher.handleExecution(executionScope);
eventProcessingFinished(executionScope, postExecutionControl);
} finally {
- metrics.reconciliationExecutionFinished(executionScope.getResource(), metricsMetadata);
+ metrics.reconciliationExecutionFinished(
+ executionScope.getResource(), executionScope.getRetryInfo(), metricsMetadata);
// restore original name
thread.setName(name);
MDCUtils.removeResourceInfo();
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java
index 411fc10e31..441d3cf178 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java
@@ -37,9 +37,9 @@
import io.javaoperatorsdk.operator.processing.LifecycleAware;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.EventSourceStartPriority;
+import io.javaoperatorsdk.operator.processing.event.source.ResourceAction;
import io.javaoperatorsdk.operator.processing.event.source.ResourceEventAware;
import io.javaoperatorsdk.operator.processing.event.source.controller.ControllerEventSource;
-import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceAction;
import io.javaoperatorsdk.operator.processing.event.source.informer.ManagedInformerEventSource;
import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource;
@@ -217,7 +217,12 @@ public Set> getRegisteredEventSources() {
@SuppressWarnings("rawtypes")
public List allEventSources() {
- return eventSources.allEventSources().toList();
+ return allEventSourcesStream().toList();
+ }
+
+ @SuppressWarnings("rawtypes")
+ public Stream allEventSourcesStream() {
+ return eventSources.allEventSources();
}
@SuppressWarnings("unused")
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java
index da4ae9835a..6e7ace0447 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ReconciliationDispatcher.java
@@ -15,25 +15,16 @@
*/
package io.javaoperatorsdk.operator.processing.event;
-import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
-import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import io.fabric8.kubernetes.api.model.HasMetadata;
-import io.fabric8.kubernetes.api.model.KubernetesResourceList;
-import io.fabric8.kubernetes.api.model.Namespaced;
-import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.client.KubernetesClientException;
-import io.fabric8.kubernetes.client.dsl.MixedOperation;
-import io.fabric8.kubernetes.client.dsl.Resource;
-import io.fabric8.kubernetes.client.dsl.base.PatchContext;
-import io.fabric8.kubernetes.client.dsl.base.PatchType;
import io.javaoperatorsdk.operator.OperatorException;
-import io.javaoperatorsdk.operator.ReconcilerUtils;
+import io.javaoperatorsdk.operator.ReconcilerUtilsInternal;
import io.javaoperatorsdk.operator.api.config.Cloner;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.BaseControl;
@@ -49,8 +40,6 @@
/** Handles calls and results of a Reconciler and finalizer related logic */
class ReconciliationDispatcher {
- public static final int MAX_UPDATE_RETRY = 10;
-
private static final Logger log = LoggerFactory.getLogger(ReconciliationDispatcher.class);
private final Controller
controller;
@@ -76,7 +65,6 @@ public ReconciliationDispatcher(Controller
controller) {
this(
controller,
new CustomResourceFacade<>(
- controller.getCRClient(),
controller.getConfiguration(),
controller.getConfiguration().getConfigurationService().getResourceCloner()));
}
@@ -84,42 +72,40 @@ public ReconciliationDispatcher(Controller
controller) {
public PostExecutionControl
handleExecution(ExecutionScope
executionScope) {
validateExecutionScope(executionScope);
try {
- return handleDispatch(executionScope);
+ return handleDispatch(executionScope, null);
} catch (Exception e) {
return PostExecutionControl.exceptionDuringExecution(e);
}
}
- private PostExecutionControl
handleDispatch(ExecutionScope
executionScope)
+ // visible for testing
+ PostExecutionControl
handleDispatch(ExecutionScope
executionScope, Context
context)
throws Exception {
P originalResource = executionScope.getResource();
var resourceForExecution = cloneResource(originalResource);
- log.debug(
- "Handling dispatch for resource name: {} namespace: {}",
- getName(originalResource),
- originalResource.getMetadata().getNamespace());
+ log.debug("Handling dispatch");
final var markedForDeletion = originalResource.isMarkedForDeletion();
if (!triggerOnAllEvents()
&& markedForDeletion
&& shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) {
- log.debug(
- "Skipping cleanup of resource {} because finalizer(s) {} don't allow processing yet",
- getName(originalResource),
- originalResource.getMetadata().getFinalizers());
+ log.debug("Skipping cleanup because finalizer(s) don't allow processing yet");
return PostExecutionControl.defaultDispatch();
}
- Context
context =
- new DefaultContext<>(
- executionScope.getRetryInfo(),
- controller,
- resourceForExecution,
- executionScope.isDeleteEvent(),
- executionScope.isDeleteFinalStateUnknown());
+ // context can be provided only for testing purposes
+ context =
+ context == null
+ ? new DefaultContext<>(
+ executionScope.getRetryInfo(),
+ controller,
+ resourceForExecution,
+ executionScope.isDeleteEvent(),
+ executionScope.isDeleteFinalStateUnknown())
+ : context;
// checking the cleaner for all-event-mode
if (!triggerOnAllEvents() && markedForDeletion) {
- return handleCleanup(resourceForExecution, originalResource, context, executionScope);
+ return handleCleanup(resourceForExecution, context, executionScope);
} else {
return handleReconcile(executionScope, resourceForExecution, originalResource, context);
}
@@ -148,11 +134,12 @@ private PostExecutionControl
handleReconcile(
*/
P updatedResource;
if (useSSA) {
- updatedResource = addFinalizerWithSSA(originalResource);
+ updatedResource = context.resourceOperations().addFinalizerWithSSA();
} else {
- updatedResource = updateCustomResourceWithFinalizer(resourceForExecution, originalResource);
+ updatedResource = context.resourceOperations().addFinalizer();
}
- return PostExecutionControl.onlyFinalizerAdded(updatedResource);
+ return PostExecutionControl.onlyFinalizerAdded(updatedResource)
+ .withReSchedule(BaseControl.INSTANT_RESCHEDULE);
} else {
try {
return reconcileExecution(executionScope, resourceForExecution, originalResource, context);
@@ -172,11 +159,7 @@ private PostExecutionControl
reconcileExecution(
P originalResource,
Context
context)
throws Exception {
- log.debug(
- "Reconciling resource {} with version: {} with execution scope: {}",
- getName(resourceForExecution),
- getVersion(resourceForExecution),
- executionScope);
+ log.debug("Reconciling resource execution scope: {}", executionScope);
UpdateControl
updateControl = controller.reconcile(resourceForExecution, context);
@@ -194,7 +177,7 @@ private PostExecutionControl
reconcileExecution(
}
if (updateControl.isPatchResource()) {
- updatedCustomResource = patchResource(toUpdate, originalResource);
+ updatedCustomResource = patchResource(context, toUpdate, originalResource);
if (!useSSA) {
toUpdate
.getMetadata()
@@ -203,7 +186,7 @@ private PostExecutionControl
reconcileExecution(
}
if (updateControl.isPatchStatus()) {
- customResourceFacade.patchStatus(toUpdate, originalResource);
+ customResourceFacade.patchStatus(context, toUpdate, originalResource);
}
return createPostExecutionControl(updatedCustomResource, updateControl, executionScope);
}
@@ -241,7 +224,7 @@ public boolean isLastAttempt() {
try {
updatedResource =
customResourceFacade.patchStatus(
- errorStatusUpdateControl.getResource().orElseThrow(), originalResource);
+ context, errorStatusUpdateControl.getResource().orElseThrow(), originalResource);
} catch (Exception ex) {
int code = ex instanceof KubernetesClientException kcex ? kcex.getCode() : -1;
Level exceptionLevel = Level.ERROR;
@@ -253,9 +236,8 @@ public boolean isLastAttempt() {
exceptionLevel = Level.DEBUG;
failedMessage = " due to conflict";
log.info(
- "ErrorStatusUpdateControl.patchStatus of {} failed due to a conflict, but the next"
- + " reconciliation is imminent.",
- ResourceID.fromResource(originalResource));
+ "ErrorStatusUpdateControl.patchStatus failed due to a conflict, but the next"
+ + " reconciliation is imminent");
} else {
exceptionLevel = Level.WARN;
failedMessage = ", but will be retried soon,";
@@ -317,15 +299,9 @@ private void updatePostExecutionControlWithReschedule(
}
private PostExecutionControl
handleCleanup(
- P resourceForExecution,
- P originalResource,
- Context
context,
- ExecutionScope
executionScope) {
+ P resourceForExecution, Context
context, ExecutionScope
executionScope) {
if (log.isDebugEnabled()) {
- log.debug(
- "Executing delete for resource: {} with version: {}",
- ResourceID.fromResource(resourceForExecution),
- getVersion(resourceForExecution));
+ log.debug("Executing delete for resource");
}
DeleteControl deleteControl = controller.cleanup(resourceForExecution, context);
final var useFinalizer = controller.useFinalizer();
@@ -334,32 +310,12 @@ private PostExecutionControl
handleCleanup(
// cleanup is finished, nothing left to be done
final var finalizerName = configuration().getFinalizerName();
if (deleteControl.isRemoveFinalizer() && resourceForExecution.hasFinalizer(finalizerName)) {
- P customResource =
- conflictRetryingPatch(
- resourceForExecution,
- originalResource,
- r -> {
- // the operator might not be allowed to retrieve the resource on a retry, e.g.
- // when its
- // permissions are removed by deleting the namespace concurrently
- if (r == null) {
- log.warn(
- "Could not remove finalizer on null resource: {} with version: {}",
- getUID(resourceForExecution),
- getVersion(resourceForExecution));
- return false;
- }
- return r.removeFinalizer(finalizerName);
- },
- true);
+ P customResource = context.resourceOperations().removeFinalizer();
return PostExecutionControl.customResourceFinalizerRemoved(customResource);
}
}
log.debug(
- "Skipping finalizer remove for resource: {} with version: {}. delete control: {}, uses"
- + " finalizer: {}",
- getUID(resourceForExecution),
- getVersion(resourceForExecution),
+ "Skipping finalizer remove for resource. Delete control: {}, uses finalizer: {}",
deleteControl,
useFinalizer);
PostExecutionControl
postExecutionControl = PostExecutionControl.defaultDispatch();
@@ -367,50 +323,10 @@ private PostExecutionControl
handleCleanup(
return postExecutionControl;
}
- @SuppressWarnings("unchecked")
- private P addFinalizerWithSSA(P originalResource) {
- log.debug(
- "Adding finalizer (using SSA) for resource: {} version: {}",
- getUID(originalResource),
- getVersion(originalResource));
- try {
- P resource = (P) originalResource.getClass().getConstructor().newInstance();
- ObjectMeta objectMeta = new ObjectMeta();
- objectMeta.setName(originalResource.getMetadata().getName());
- objectMeta.setNamespace(originalResource.getMetadata().getNamespace());
- resource.setMetadata(objectMeta);
- resource.addFinalizer(configuration().getFinalizerName());
- return customResourceFacade.patchResourceWithSSA(resource);
- } catch (InstantiationException
- | IllegalAccessException
- | InvocationTargetException
- | NoSuchMethodException e) {
- throw new RuntimeException(
- "Issue with creating custom resource instance with reflection."
- + " Custom Resources must provide a no-arg constructor. Class: "
- + originalResource.getClass().getName(),
- e);
+ private P patchResource(Context
context, P resource, P originalResource) {
+ if (log.isDebugEnabled()) {
+ log.debug("Updating resource; with SSA: {}", useSSA);
}
- }
-
- private P updateCustomResourceWithFinalizer(P resourceForExecution, P originalResource) {
- log.debug(
- "Adding finalizer for resource: {} version: {}",
- getUID(originalResource),
- getVersion(originalResource));
- return conflictRetryingPatch(
- resourceForExecution,
- originalResource,
- r -> r.addFinalizer(configuration().getFinalizerName()),
- false);
- }
-
- private P patchResource(P resource, P originalResource) {
- log.debug(
- "Updating resource: {} with version: {}; SSA: {}",
- getUID(resource),
- getVersion(resource),
- useSSA);
log.trace("Resource before update: {}", resource);
final var finalizerName = configuration().getFinalizerName();
@@ -418,64 +334,13 @@ private P patchResource(P resource, P originalResource) {
// addFinalizer already prevents adding an already present finalizer so no need to check
resource.addFinalizer(finalizerName);
}
- return customResourceFacade.patchResource(resource, originalResource);
+ return customResourceFacade.patchResource(context, resource, originalResource);
}
ControllerConfiguration
configuration() {
return controller.getConfiguration();
}
- public P conflictRetryingPatch(
- P resource,
- P originalResource,
- Function
modificationFunction,
- boolean forceNotUseSSA) {
- if (log.isDebugEnabled()) {
- log.debug("Conflict retrying update for: {}", ResourceID.fromResource(resource));
- }
- int retryIndex = 0;
- while (true) {
- try {
- var modified = modificationFunction.apply(resource);
- if (Boolean.FALSE.equals(modified)) {
- return resource;
- }
- if (forceNotUseSSA) {
- return customResourceFacade.patchResourceWithoutSSA(resource, originalResource);
- } else {
- return customResourceFacade.patchResource(resource, originalResource);
- }
- } catch (KubernetesClientException e) {
- log.trace("Exception during patch for resource: {}", resource);
- retryIndex++;
- // only retry on conflict (409) and unprocessable content (422) which
- // can happen if JSON Patch is not a valid request since there was
- // a concurrent request which already removed another finalizer:
- // List element removal from a list is by index in JSON Patch
- // so if addressing a second finalizer but first is meanwhile removed
- // it is a wrong request.
- if (e.getCode() != 409 && e.getCode() != 422) {
- throw e;
- }
- if (retryIndex >= MAX_UPDATE_RETRY) {
- throw new OperatorException(
- "Exceeded maximum ("
- + MAX_UPDATE_RETRY
- + ") retry attempts to patch resource: "
- + ResourceID.fromResource(resource));
- }
- log.debug(
- "Retrying patch for resource name: {}, namespace: {}; HTTP code: {}",
- resource.getMetadata().getName(),
- resource.getMetadata().getNamespace(),
- e.getCode());
- resource =
- customResourceFacade.getResource(
- resource.getMetadata().getNamespace(), resource.getMetadata().getName());
- }
- }
- }
-
private void validateExecutionScope(ExecutionScope
executionScope) {
if (!triggerOnAllEvents()
&& (executionScope.isDeleteEvent() || executionScope.isDeleteFinalStateUnknown())) {
@@ -488,70 +353,41 @@ private void validateExecutionScope(ExecutionScope
executionScope) {
// created to support unit testing
static class CustomResourceFacade {
- private final MixedOperation, Resource