Skip to content

Comments

fix: Add operator flags in setup#19

Open
Agent-Hellboy wants to merge 3 commits intomainfrom
add_operator_flags_in_setup
Open

fix: Add operator flags in setup#19
Agent-Hellboy wants to merge 3 commits intomainfrom
add_operator_flags_in_setup

Conversation

@Agent-Hellboy
Copy link
Owner

@Agent-Hellboy Agent-Hellboy commented Dec 22, 2025

Summary by CodeRabbit

  • New Features

    • Setup command accepts operator startup flags (metrics/probe addresses, leader-election) and passes them through to deployments so the operator can start with specified args.
  • Bug Fixes

    • Status updates made more concurrency-safe with improved error handling and retry behavior.
  • Tests

    • Tests updated to exercise status subresource handling and propagation of operator startup args.
  • Documentation

    • CLI help formatting improved and new operator flags documented.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 22, 2025

📝 Walkthrough

Walkthrough

Adds three operator CLI flags collected into an operatorArgs slice, propagates operatorArgs through SetupPlan and deploy steps, injects operatorArgs into the operator manager deployment YAML, and hardens operator status updates to handle resourceVersion conflicts and NotFound cases.

Changes

Cohort / File(s) Summary
CLI: flags, arg builder, and YAML injection
internal/cli/setup.go
Added flags --operator-metrics-addr, --operator-probe-addr, --operator-leader-elect; implemented buildOperatorArgs; added injectOperatorArgs; changed DeployOperatorManifests signature to accept operatorArgs []string.
Plan: carry operator args in plan
internal/cli/setup_plan.go
Added OperatorArgs []string to SetupPlanInput and SetupPlan; propagated OperatorArgs in BuildSetupPlan.
Deployment step wiring
internal/cli/setup_steps.go, internal/cli/setup.go
Forward ctx.Plan.OperatorArgs into deployOperatorStep and onward to deploy functions.
Operator manifests deployment
internal/cli/setup.go (deployOperatorManifests, deployOperatorManifestsWithKubectl)
Updated functions to accept and forward operatorArgs; inject operatorArgs into manager deployment YAML when present before applying manifests.
Operator controller: status update robustness
internal/operator/controller.go
updateStatus now re-fetches the MCPServer to get latest resourceVersion, handles NotFound by skipping, uses Status().Update() and falls back to full Update on non-conflict failures, and logs conflicts for retry.
Tests: adapt to new signatures and status behavior
internal/cli/setup_helpers_test.go, internal/cli/setup_plan_test.go, internal/operator/controller_test.go
Updated test call sites and mocks to include the new []string operatorArgs parameter; controller tests create MCPServer with status subresource and assert on fetched updated status.
CLI help golden
test/golden/cli/testdata/mcp-runtime_setup_help.golden
Reformatted flag alignment and added the three new operator flags to the CLI help golden file.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant CLI as "mcp-runtime setup (flags)"
    participant Builder as "BuildSetupPlan"
    participant Plan as "SetupPlan"
    participant Step as "deployOperatorStep"
    participant Deployer as "DeployOperatorManifests"
    participant YAML as "injectOperatorArgs (manager YAML)"

    User->>CLI: run setup with operator flags
    CLI->>CLI: buildOperatorArgs() -> operatorArgs
    CLI->>Builder: BuildSetupPlan(OperatorArgs)
    Builder->>Plan: return SetupPlan (OperatorArgs)
    CLI->>Step: invoke deployOperatorStep(ctx.Plan.OperatorArgs)
    Step->>Deployer: call DeployOperatorManifests(operatorImage, operatorArgs)
    Deployer->>YAML: injectOperatorArgs(manager.yaml, operatorArgs)
    YAML-->>Deployer: manager.yaml with args injected
    Deployer->>Deployer: apply manifests (operator starts with args)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through flags and threaded each one through,
I tucked them in YAML so the manager knew,
From CLI to plan, through steps they slid,
The operator wakes with arguments amid. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding operator flags to the setup command. It directly reflects the core modifications across the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add_operator_flags_in_setup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/cli/setup_helpers_test.go (1)

498-499: Tests updated for new signature, but consider adding operatorArgs coverage.

The test call sites correctly pass nil for the new operatorArgs parameter to accommodate the updated signature.

Consider adding a test case that verifies deployOperatorManifestsWithKubectl correctly injects operator arguments into the manager YAML when operatorArgs is non-nil.

Also applies to: 553-553, 575-575, 612-612

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f8a11d and c7a3883.

📒 Files selected for processing (5)
  • internal/cli/setup.go
  • internal/cli/setup_helpers_test.go
  • internal/cli/setup_plan.go
  • internal/cli/setup_plan_test.go
  • internal/cli/setup_steps.go
🧰 Additional context used
🧬 Code graph analysis (1)
internal/cli/setup.go (3)
internal/cli/registry.go (1)
  • ExternalRegistryConfig (201-205)
internal/cli/printer.go (1)
  • Info (257-257)
internal/cli/kubectl_runner.go (1)
  • KubectlRunner (6-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit + Integration Tests
🔇 Additional comments (7)
internal/cli/setup_plan.go (1)

13-14: LGTM!

The OperatorArgs field is correctly added to both input and plan structs, and properly propagated through BuildSetupPlan. Clean implementation for passing operator arguments through the setup flow.

Also applies to: 24-25, 54-55

internal/cli/setup_steps.go (1)

100-110: LGTM!

The deployOperatorStepCmd.Run correctly forwards ctx.Plan.OperatorArgs to the deployOperatorStep function, maintaining consistency with the updated function signature.

internal/cli/setup_plan_test.go (1)

152-152: LGTM!

The mock function signatures for DeployOperatorManifests are correctly updated to accept the new []string parameter for operator arguments, maintaining consistency with the updated interface.

Also applies to: 222-222, 302-302

internal/cli/setup.go (4)

126-128: LGTM!

The operator configuration flags are well-named with the operator- prefix for clarity, and using empty string defaults allows the manager.yaml defaults to apply when flags aren't explicitly set.

Also applies to: 168-170


343-348: LGTM!

The function signatures for deployOperatorStep, deployOperatorManifests, and deployOperatorManifestsWithKubectl are consistently updated to accept the new operatorArgs parameter, and the arguments are correctly passed through the call chain.

Also applies to: 643-649


681-684: LGTM!

The conditional check before calling injectOperatorArgs correctly avoids unnecessary processing when no operator arguments are provided.


175-191: No issues found with the buildOperatorArgs function.

The flag names (--metrics-bind-address, --health-probe-bind-address, --leader-elect) are correct and standard for controller-runtime operators. The function logic properly handles conditional argument inclusion based on non-empty and non-default values, preserving manager.yaml defaults.

Comment on lines +716 to +754
// injectOperatorArgs injects operator command-line arguments into the manager deployment YAML.
// It replaces the existing args section or adds one if it doesn't exist.
func injectOperatorArgs(yamlContent string, args []string) string {
if len(args) == 0 {
return yamlContent
}

// Build the new args section
indent := " " // 8 spaces (standard YAML indentation for args in containers)
argsYAML := indent + "args:\n"
for _, arg := range args {
argsYAML += fmt.Sprintf("%s- %s\n", indent, arg)
}

// Pattern to match the args section - find "args:" line and everything after it until next key
// Match: "args:" followed by any number of indented list items (using [0-9] instead of backreference)
argsPattern := regexp.MustCompile(`(?m)^(\s*)args:\s*$\n((?:\s+-\s+[^\n]+\n?)*)`)
if argsPattern.MatchString(yamlContent) {
// Replace existing args section - capture the indentation from group 1
return argsPattern.ReplaceAllString(yamlContent, "${1}args:\n"+argsYAML)
}

// If no args section exists, find the command line and add args after it
// Pattern: matches "command:" followed by command array items
commandPattern := regexp.MustCompile(`(?m)^(\s*)command:\s*$\n((?:\s+-\s+[^\n]+\n?)+)`)
if commandPattern.MatchString(yamlContent) {
// Add args after command
return commandPattern.ReplaceAllString(yamlContent, "${0}"+argsYAML)
}

// Fallback: if we can't find command, try to add after image
imagePattern := regexp.MustCompile(`(?m)^(\s*)image:\s*\S+$`)
if imagePattern.MatchString(yamlContent) {
return imagePattern.ReplaceAllString(yamlContent, "${0}\n"+argsYAML)
}

// If all else fails, return original (shouldn't happen with valid manager.yaml)
return yamlContent
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Duplicate "args:" header in YAML injection causes malformed output.

The injectOperatorArgs function has a bug that produces invalid YAML. At line 724-728, argsYAML is built to include "args:\n" as a prefix, but then:

  1. Line 735: When replacing an existing args section, the replacement is "${1}args:\n"+argsYAML, which produces:

    args:
            args:
            - --metrics-bind-address=:9090
  2. Lines 743 and 749: The fallback patterns append argsYAML which already contains the args: header, which is correct for adding a new section but will have wrong indentation.

🔎 Proposed fix
 func injectOperatorArgs(yamlContent string, args []string) string {
 	if len(args) == 0 {
 		return yamlContent
 	}

-	// Build the new args section
-	indent := "        " // 8 spaces (standard YAML indentation for args in containers)
-	argsYAML := indent + "args:\n"
+	// Build the args items (without the "args:" header)
+	indent := "        " // 8 spaces (standard YAML indentation for args in containers)
+	var argsItems string
 	for _, arg := range args {
-		argsYAML += fmt.Sprintf("%s- %s\n", indent, arg)
+		argsItems += fmt.Sprintf("%s- %s\n", indent, arg)
 	}

 	// Pattern to match the args section - find "args:" line and everything after it until next key
 	// Match: "args:" followed by any number of indented list items (using [0-9] instead of backreference)
 	argsPattern := regexp.MustCompile(`(?m)^(\s*)args:\s*$\n((?:\s+-\s+[^\n]+\n?)*)`)
 	if argsPattern.MatchString(yamlContent) {
 		// Replace existing args section - capture the indentation from group 1
-		return argsPattern.ReplaceAllString(yamlContent, "${1}args:\n"+argsYAML)
+		return argsPattern.ReplaceAllString(yamlContent, "${1}args:\n"+argsItems)
 	}

+	// Full args section for adding new (includes header)
+	argsSection := indent + "args:\n" + argsItems
+
 	// If no args section exists, find the command line and add args after it
 	// Pattern: matches "command:" followed by command array items
 	commandPattern := regexp.MustCompile(`(?m)^(\s*)command:\s*$\n((?:\s+-\s+[^\n]+\n?)+)`)
 	if commandPattern.MatchString(yamlContent) {
 		// Add args after command
-		return commandPattern.ReplaceAllString(yamlContent, "${0}"+argsYAML)
+		return commandPattern.ReplaceAllString(yamlContent, "${0}"+argsSection)
 	}

 	// Fallback: if we can't find command, try to add after image
 	imagePattern := regexp.MustCompile(`(?m)^(\s*)image:\s*\S+$`)
 	if imagePattern.MatchString(yamlContent) {
-		return imagePattern.ReplaceAllString(yamlContent, "${0}\n"+argsYAML)
+		return imagePattern.ReplaceAllString(yamlContent, "${0}\n"+argsSection)
 	}

 	// If all else fails, return original (shouldn't happen with valid manager.yaml)
 	return yamlContent
 }
🤖 Prompt for AI Agents
internal/cli/setup.go lines 716-754: The function currently builds argsYAML
including an "args:\n" header and then inserts it in places that also add their
own "args:" header (causing duplicate headers) and uses a fixed indent; fix by
separating the header from the list items: build a string of arg list items only
(e.g., lines like "- <arg>\n") and when replacing or inserting, prepend a single
"args:\n" with the correct captured indentation from the regex match (use the
capture group indentation for list items as well) — for the existing-args
replacement use "${1}args:\n"+argItems, for the command insertion prepend the
match's indentation + "args:\n"+argItems, and for the image fallback append
"\n"+capturedIndent+"args:\n"+argItems; remove the duplicate hardcoded header
and ensure indentation is derived from regex group 1.

@codecov
Copy link

codecov bot commented Dec 22, 2025

Codecov Report

❌ Patch coverage is 19.35484% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.60%. Comparing base (1f8a11d) to head (86a9a29).

Files with missing lines Patch % Lines
internal/cli/setup.go 4.87% 37 Missing and 2 partials ⚠️
internal/operator/controller.go 42.10% 7 Missing and 4 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #19      +/-   ##
==========================================
- Coverage   81.96%   80.60%   -1.36%     
==========================================
  Files          22       22              
  Lines        2744     2794      +50     
==========================================
+ Hits         2249     2252       +3     
- Misses        346      388      +42     
- Partials      149      154       +5     
Flag Coverage Δ
post-merge ?
pre-merge 80.60% <19.35%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
internal/cli/setup_plan.go 100.00% <100.00%> (ø)
internal/cli/setup_steps.go 100.00% <100.00%> (ø)
internal/operator/controller.go 74.56% <42.10%> (-2.03%) ⬇️
internal/cli/setup.go 58.77% <4.87%> (-5.04%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
internal/operator/controller.go (2)

640-642: Consider skipping status update on fetch errors instead of fallback.

When fetching the latest MCPServer fails (non-NotFound errors), the code falls back to using the original mcpServer object. However, this object is likely stale and will cause a conflict error during the Status().Update() call, creating unnecessary retry overhead.

Consider returning early on fetch errors (similar to the NotFound case) and letting the next reconcile retry with fresh data.

🔎 Suggested refactor
 	if err := r.Get(ctx, types.NamespacedName{Name: mcpServer.Name, Namespace: mcpServer.Namespace}, latest); err != nil {
 		// If object not found, it may have been deleted - skip status update
 		if errors.IsNotFound(err) {
 			log.FromContext(ctx).V(1).Info("MCPServer not found, skipping status update (may have been deleted)")
 			return
 		}
-		// For other errors, log but try to update with the original object as fallback
-		log.FromContext(ctx).Error(err, "Failed to fetch MCPServer for status update, using original object")
-		latest = mcpServer
+		// For other errors, skip status update and let next reconcile retry
+		log.FromContext(ctx).Error(err, "Failed to fetch MCPServer for status update, skipping update")
+		return
 	}

659-662: Fallback Update() may inadvertently modify spec fields.

When Status().Update() fails, the code falls back to a full Update() on the latest object. However, this latest object was fetched via Get(), which includes both spec and status. If there's a mismatch between the spec in latest and the actual desired spec, this fallback could inadvertently revert spec changes made elsewhere.

Consider explicitly setting only status fields before the fallback Update(), or document this limitation.

🔎 Proposed refinement
 		} else {
 			// Some environments (like the fake client) may not support status subresources.
+			// Note: This performs a full object update, not just status.
+			// In rare cases, this could revert concurrent spec changes.
 			if updateErr := r.Update(ctx, latest); updateErr != nil {
 				log.FromContext(ctx).Error(updateErr, "Failed to update MCPServer after status update failure", "resourceVersion", latest.ResourceVersion)
 			}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7a3883 and 86a9a29.

📒 Files selected for processing (3)
  • internal/operator/controller.go
  • internal/operator/controller_test.go
  • test/golden/cli/testdata/mcp-runtime_setup_help.golden
🧰 Additional context used
🧬 Code graph analysis (2)
internal/operator/controller_test.go (1)
api/v1alpha1/mcpserver_types.go (1)
  • MCPServer (120-126)
internal/operator/controller.go (1)
api/v1alpha1/mcpserver_types.go (1)
  • MCPServer (120-126)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit + Integration Tests
🔇 Additional comments (3)
internal/operator/controller_test.go (2)

274-280: LGTM: Test updated to support status subresource.

The test now correctly initializes the fake client with status subresource support and explicitly creates the MCPServer object. This aligns with the controller's refactored updateStatus method that uses Status().Update().


474-482: LGTM: Test correctly verifies status update via API.

The test now fetches the updated MCPServer object from the client and verifies the status fields, which correctly reflects the behavior of the refactored updateStatus method that persists status via the API.

test/golden/cli/testdata/mcp-runtime_setup_help.golden (1)

18-20: Verify default values match operator implementation.

The help text references default values from manager.yaml for the new operator flags. Ensure these defaults (:8080 for metrics, :8081 for probes, false for leader election) match the actual defaults in the operator manager deployment manifest.

Run the following script to verify the default values in the operator configuration:

#!/bin/bash
# Description: Check default values in operator manager YAML files

# Search for manager deployment manifests
fd -e yaml -e yml manager config/

# Search for default flag values in Go code
rg -nP '\bmetrics.*addr\b|\bprobe.*addr\b|\bleader.*elect\b' --type=go -A2 -B2

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cli/setup.go (1)

408-423: Critical: Missing operatorArgs parameter breaks the feature.

The deployOperatorStep function receives operatorArgs as a parameter (line 407) but fails to pass it to deps.DeployOperatorManifests at line 409. This means the operator command-line arguments will never be applied to the deployment, completely breaking the new feature.

🔎 Proposed fix
 	Info("Deploying operator manifests")
-	if err := deps.DeployOperatorManifests(logger, operatorImage); err != nil {
+	if err := deps.DeployOperatorManifests(logger, operatorImage, operatorArgs); err != nil {
 		wrappedErr := wrapWithSentinelAndContext(
♻️ Duplicate comments (1)
internal/cli/setup.go (1)

921-959: Critical: Duplicate "args:" header in YAML injection.

This issue was already identified in a previous review. The function builds argsYAML with an "args:\n" prefix (line 930), but then the replacement at line 940 also adds "args:\n", resulting in duplicate headers and malformed YAML output.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86a9a29 and 318a43b.

📒 Files selected for processing (3)
  • internal/cli/setup.go
  • internal/cli/setup_steps.go
  • internal/operator/controller.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/cli/setup_steps.go
🧰 Additional context used
🧬 Code graph analysis (2)
internal/cli/setup.go (2)
internal/cli/registry.go (1)
  • ExternalRegistryConfig (229-233)
internal/cli/kubectl_runner.go (1)
  • KubectlRunner (6-10)
internal/operator/controller.go (1)
api/v1alpha1/mcpserver_types.go (1)
  • MCPServer (120-126)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit + Integration Tests
🔇 Additional comments (9)
internal/operator/controller.go (1)

663-686: LGTM: Robust status update preparation.

The re-fetch logic properly handles the latest resourceVersion and avoids optimistic locking conflicts. The NotFound case is handled gracefully, and the fallback to the original object on fetch errors is reasonable.

internal/cli/setup.go (8)

127-129: LGTM: Flag variable declarations are appropriate.

The three operator flag variables are correctly typed and positioned.


143-144: LGTM: Operator args construction.

The call to buildOperatorArgs correctly assembles operator arguments from the CLI flags.


176-192: LGTM: Clean flag-to-args conversion.

The function correctly constructs operator arguments, including only explicitly set flags. The logic appropriately handles empty strings and boolean defaults.


407-407: LGTM: Function signature updated correctly.

The operatorArgs []string parameter is appropriately added to the function signature.


793-799: LGTM: Function signatures consistently updated.

Both deployOperatorManifests and deployOperatorManifestsWithKubectl signatures are correctly updated to accept operatorArgs []string. The comment at line 798 appropriately reflects the new behavior.


856-859: LGTM: Conditional injection preserves backward compatibility.

The logic correctly injects operator args only when provided, ensuring existing behavior is unchanged when no flags are specified.


45-45: LGTM: Interface signature updated correctly.

The DeployOperatorManifests function type in SetupDeps is appropriately updated to include the operatorArgs []string parameter.


169-171: The code is correctly designed. The operator- prefixed flags at the CLI level (lines 169-171) are intentional aliases that get translated to standard controller-runtime flag names (--metrics-bind-address, --health-probe-bind-address, --leader-elect) by the buildOperatorArgs() function (lines 182, 185, 188) before being passed to the operator. This layering avoids flag name conflicts with other setup flags and maintains compliance with controller-runtime conventions. No changes needed.

Comment on lines +688 to 703
// Use Status().Update() which only updates the status subresource
// This is safer than Update() which would update the entire object
if err := r.Status().Update(ctx, latest); err != nil {
// If it's a conflict error, that's expected in concurrent scenarios - log at debug level
if errors.IsConflict(err) {
log.FromContext(ctx).V(1).Info("Status update conflict (expected in concurrent reconciles), will retry on next reconcile", "resourceVersion", latest.ResourceVersion)
} else {
// Some environments (like the fake client) may not support status subresources.
if updateErr := r.Update(ctx, latest); updateErr != nil {
log.FromContext(ctx).Error(updateErr, "Failed to update MCPServer after status update failure", "resourceVersion", latest.ResourceVersion)
}
// Log other errors but don't fail the reconcile - status update failures are non-fatal
// The next reconcile will retry the status update
log.FromContext(ctx).Error(err, "Failed to update MCPServer status", "resourceVersion", latest.ResourceVersion)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reconsider the fallback to full Update().

The fallback to r.Update(ctx, latest) on line 696 updates the entire object, potentially overwriting concurrent spec changes. While the comment mentions fake client compatibility, in production Kubernetes, if Status().Update() fails for reasons other than unsupported status subresource (e.g., validation errors, admission webhook failures), falling back to a full update could cause unintended side effects.

Consider either:

  1. Removing the fallback entirely and just logging the error (status updates will retry on next reconcile).
  2. Detecting the fake client environment explicitly and only using the fallback in tests.
  3. Checking the specific error type to determine if fallback is safe.
🔎 Option 1: Remove the fallback (recommended)
 	if err := r.Status().Update(ctx, latest); err != nil {
 		// If it's a conflict error, that's expected in concurrent scenarios - log at debug level
 		if errors.IsConflict(err) {
 			log.FromContext(ctx).V(1).Info("Status update conflict (expected in concurrent reconciles), will retry on next reconcile", "resourceVersion", latest.ResourceVersion)
 		} else {
-			// Some environments (like the fake client) may not support status subresources.
-			if updateErr := r.Update(ctx, latest); updateErr != nil {
-				log.FromContext(ctx).Error(updateErr, "Failed to update MCPServer after status update failure", "resourceVersion", latest.ResourceVersion)
-			}
 			// Log other errors but don't fail the reconcile - status update failures are non-fatal
 			// The next reconcile will retry the status update
 			log.FromContext(ctx).Error(err, "Failed to update MCPServer status", "resourceVersion", latest.ResourceVersion)
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Use Status().Update() which only updates the status subresource
// This is safer than Update() which would update the entire object
if err := r.Status().Update(ctx, latest); err != nil {
// If it's a conflict error, that's expected in concurrent scenarios - log at debug level
if errors.IsConflict(err) {
log.FromContext(ctx).V(1).Info("Status update conflict (expected in concurrent reconciles), will retry on next reconcile", "resourceVersion", latest.ResourceVersion)
} else {
// Some environments (like the fake client) may not support status subresources.
if updateErr := r.Update(ctx, latest); updateErr != nil {
log.FromContext(ctx).Error(updateErr, "Failed to update MCPServer after status update failure", "resourceVersion", latest.ResourceVersion)
}
// Log other errors but don't fail the reconcile - status update failures are non-fatal
// The next reconcile will retry the status update
log.FromContext(ctx).Error(err, "Failed to update MCPServer status", "resourceVersion", latest.ResourceVersion)
}
}
if err := r.Status().Update(ctx, latest); err != nil {
// If it's a conflict error, that's expected in concurrent scenarios - log at debug level
if errors.IsConflict(err) {
log.FromContext(ctx).V(1).Info("Status update conflict (expected in concurrent reconciles), will retry on next reconcile", "resourceVersion", latest.ResourceVersion)
} else {
// Log other errors but don't fail the reconcile - status update failures are non-fatal
// The next reconcile will retry the status update
log.FromContext(ctx).Error(err, "Failed to update MCPServer status", "resourceVersion", latest.ResourceVersion)
}
}
🤖 Prompt for AI Agents
In internal/operator/controller.go around lines 688 to 703, remove the fallback
call to r.Update(ctx, latest) inside the Status().Update() error handling
because a full Update can overwrite concurrent spec changes; instead, only
handle conflict errors at debug level and log other Status() update errors
without attempting a full Update. If tests require a fallback, restrict that
behavior to test environments only (e.g., detect the fake client or use
build-tags/test helpers) rather than performing a full Update in production.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant