/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..83acda1d8d
--- /dev/null
+++ b/observability/josdk-operator-metrics-dashboard.json
@@ -0,0 +1,1175 @@
+{
+ "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(operator_sdk_reconciliations_started_total{service_name=\"josdk\"}[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(operator_sdk_reconciliations_success_total{service_name=\"josdk\"}[5m])) by (controller_name)",
+ "legendFormat": "Success - {{controller_name}}",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(operator_sdk_reconciliations_failure_total{service_name=\"josdk\"}[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(operator_sdk_reconciliations_executions{service_name=\"josdk\"})",
+ "legendFormat": "Executing",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Currently Executing Reconciliations",
+ "type": "gauge"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Current reconciliation queue size",
+ "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(operator_sdk_reconciliations_active{service_name=\"josdk\"})",
+ "legendFormat": "Active",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Active Reconciliations",
+ "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(operator_sdk_reconciliations_started_total{service_name=\"josdk\"})",
+ "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(operator_sdk_reconciliations_failure_total{service_name=\"josdk\"}[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=\"josdk\"}",
+ "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(operator_sdk_reconciliations_execution_seconds_bucket{service_name=\"josdk\"}[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(operator_sdk_reconciliations_execution_seconds_bucket{service_name=\"josdk\"}[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(operator_sdk_reconciliations_execution_seconds_bucket{service_name=\"josdk\"}[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(operator_sdk_events_received_total{service_name=\"josdk\"}[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(operator_sdk_reconciliations_failure_total{service_name=\"josdk\"}[5m])) by (controller_name)",
+ "legendFormat": "{{controller_name}}",
+ "range": true,
+ "refId": "A"
+ }
+ ],
+ "title": "Failures by Controller",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Controller execution success vs failure",
+ "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": 32
+ },
+ "id": 10,
+ "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(operator_sdk_controllers_success_total{service_name=\"josdk\"}[5m])) by (controller_name)",
+ "legendFormat": "Success - {{controller_name}}",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "sum(rate(operator_sdk_controllers_failure_total{service_name=\"josdk\"}[5m])) by (controller_name)",
+ "legendFormat": "Failure - {{controller_name}}",
+ "range": true,
+ "refId": "B"
+ }
+ ],
+ "title": "Controller Execution Success vs Failure",
+ "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(operator_sdk_events_delete_total{service_name=\"josdk\"}[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(operator_sdk_reconciliations_retries_total{service_name=\"josdk\"}[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": []
+ },
+ "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 8793a087fa..93949d794b 100644
--- a/operator-framework-bom/pom.xml
+++ b/operator-framework-bom/pom.xml
@@ -21,7 +21,7 @@
io.javaoperatorsdk
operator-framework-bom
- 5.2.3-SNAPSHOT
+ 5.3.0-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 3b9bcbc471..72fe2e8188 100644
--- a/operator-framework-core/pom.xml
+++ b/operator-framework-core/pom.xml
@@ -21,7 +21,7 @@
io.javaoperatorsdk
java-operator-sdk
- 5.2.3-SNAPSHOT
+ 5.3.0-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/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..ae30068cf2 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,7 +37,7 @@
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;
@@ -298,7 +298,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.");
@@ -372,20 +371,19 @@ 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);
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);
retryEventSource().scheduleOnce(resourceID, delay);
},
() -> {
@@ -550,7 +548,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..010b161979 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,13 +72,14 @@ 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);
@@ -109,17 +98,20 @@ && shouldNotDispatchToCleanupWhenMarkedForDeletion(originalResource)) {
originalResource.getMetadata().getFinalizers());
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 +140,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);
@@ -194,7 +187,7 @@ private PostExecutionControl
reconcileExecution(
}
if (updateControl.isPatchResource()) {
- updatedCustomResource = patchResource(toUpdate, originalResource);
+ updatedCustomResource = patchResource(context, toUpdate, originalResource);
if (!useSSA) {
toUpdate
.getMetadata()
@@ -203,7 +196,7 @@ private PostExecutionControl
reconcileExecution(
}
if (updateControl.isPatchStatus()) {
- customResourceFacade.patchStatus(toUpdate, originalResource);
+ customResourceFacade.patchStatus(context, toUpdate, originalResource);
}
return createPostExecutionControl(updatedCustomResource, updateControl, executionScope);
}
@@ -241,7 +234,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;
@@ -317,10 +310,7 @@ 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: {}",
@@ -334,24 +324,7 @@ 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);
}
}
@@ -367,50 +340,14 @@ 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 version: {}; SSA: {}",
+ resource.getMetadata().getName(),
+ getVersion(resource),
+ 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 +355,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,34 +374,15 @@ private void validateExecutionScope(ExecutionScope
executionScope) {
// created to support unit testing
static class CustomResourceFacade {
- private final MixedOperation, Resource> resourceOperation;
private final boolean useSSA;
- private final String fieldManager;
private final Cloner cloner;
- public CustomResourceFacade(
- MixedOperation, Resource