Contributing to Operator, MeshSync & Broker Settings

Meshery Operator, MeshSync and Meshery Broker are configured through a layered document, MesheryControllersConfig, defined in meshery/schemas as the controllers_config construct (v1alpha1). This page is the authoritative map of that surface for contributors: every setting, its storage, its reader, and its observable effect.

Keep it current. A setting added without a row here is a setting nobody can find, and a row without an end-to-end test is a setting nobody can trust.

The three layers

Configuration resolves through a fixed precedence chain. Every field is independently layered - leaving a field unset at one layer defers to the next.

PrecedenceLayerStored inSet from
1 (highest)Per-connection overrideconnection.metadata.controllers_configConnections table -> row actions -> Configure Operator, MeshSync & Broker
2Server-wide defaultsystem_settings row controllers_config_defaultsSettings -> Operator, MeshSync & Broker tab
3 (lowest)Built-in defaultCompiled in: connections.BuiltInControllersConfig()Not settable

connections.ResolveControllersConfig returns two documents:

  • merged - only fields explicitly set at layer 1 or 2. This is what propagates to the cluster. Fields absent here are withdrawn on the next apply, reverting to the operator’s or chart’s own value.
  • effective - merged overlaid onto the built-in defaults. This is the complete resolved view the API returns to clients.

The UI mirrors the built-in defaults in BUILT_IN_CONTROLLERS_CONFIG (ui/components/configuration/ControllersConfigForm.tsx). The two must agree; they are asserted against each other by unit test.

Collections merge atomically

Scalars merge per leaf. Collections - meshsync.watchList, meshsync.outputNamespaces, meshsync.outputResources, broker.service.annotations, broker.service.loadBalancerSourceRanges - merge whole: a layer that sets one replaces the lower layer’s value entirely. Element-wise merging of a whitelist or a namespace filter would produce a scope neither layer asked for.

The settings

Every row is a distinct behavior. “Propagates to” names the cluster object the server writes; “Observable as” names what the user can actually see change.

Meshery Operator

SettingWire pathPropagates toObservable as
Deployment modeoperator.deploymentModeDeploy/undeploy of the operator; MeshSync data pipeline teardown and reattachConnection detail “MeshSync Deployment Mode”; operator/MeshSync/broker status chips in the header
Operator versionoperator.versionmeshery-operator Helm release chart versionOperator version in the header status chip; meshery-operator pod image

deploymentMode is one of operator (Meshery Operator installs MeshSync and Broker into the cluster) or embedded (MeshSync runs in-process inside Meshery Server; nothing is installed into the cluster). The built-in default is embedded.

Where operator.version is read

connections.OperatorChartVersionFromControllersConfig is the single reader. It feeds MesheryControllersHelper.operatorDeploymentConfig, which overlays the resolved value onto the controllers.OperatorDeploymentConfig assembled once at boot by models.NewOperatorDeploymentConfig. That struct is what the meshkit operator controller handler is constructed with, and its MesheryReleaseVersion is the chart version passed to the meshery-operator Helm release.

Two consequences shape the code around it:

  • The handler captures the chart version at construction, so SetControllersConfig must run before AddCtxControllerHandlers. Every call site orders them that way.
  • A version changed on a live connection therefore cannot take effect by re-applying the document. MesheryControllersHelper.ReconcileOperatorChartVersion re-attaches the handlers and re-runs the Helm release (installed with UpgradeIfInstalled, so this is an in-place upgrade). It is a no-op in embedded mode, when the operator is disabled server-wide or explicitly undeployed for the context, and when the resolved version already matches the attached handler’s - so a chart-version change never resurrects an operator the user turned off.

Leaving the field unset at every layer resolves to "", and the boot-time chart version applies unchanged: a connection on Inherit behaves exactly as it did before the field was wired up.

MeshSync

SettingWire pathPropagates toObservable as
Versionmeshsync.versionMeshSync CR spec.versionMeshSync version in the header status chip; MeshSync pod image
Replicasmeshsync.replicasMeshSync CR spec.sizeMeshSync pod count
Watch listmeshsync.watchListMeshSync CR spec.watch-list.data.{whitelist,blacklist} + rolling restartWhich resources appear under Dashboard -> Resources
Output namespacesmeshsync.outputNamespacesMeshSync Deployment arg --outputNamespacesWhich namespaces’ resources are published
Output resourcesmeshsync.outputResourcesMeshSync Deployment arg --outputResourcesWhich kinds are published
Secret redactionmeshsync.redactSecretsMeshSync Deployment env MESHSYNC_REDACT_SECRETSSecret values redacted in Dashboard -> Resources
Broker content dedupmeshsync.brokerContentDedupMeshSync Deployment env MESHSYNC_BROKER_CONTENT_DEDUPFewer duplicate MeshSync events in the notification center
Debug loggingmeshsync.debugLoggingMeshSync Deployment env DEBUGMeshSync pod logs

watchList accepts at most one of whitelist or blacklist; both set is rejected. MeshSync reads its watch-list at startup only, so a watch-list change also stamps meshery.io/restarted-at on the MeshSync Deployment pod template, which rolls the pods.

Be precise about what that guarantee covers. Every setting in this section that propagates to the Deployment - the three env vars and the two output-filter args

  • lives in the pod template, so changing any of them triggers an ordinary rolling update by definition; that is Kubernetes, not something Meshery chooses. What the annotation handling guarantees is narrower: meshery.io/restarted-at is refreshed only when the watch-list changes, and the previously-applied value is otherwise carried forward unchanged, so an apply that changes nothing in the pod template does not roll pods gratuitously. A watch-list change is the only thing that forces a restart when the template is otherwise identical.

Version, replicas and watch-list are operator-mode only: they live on the MeshSync custom resource, which embedded-mode clusters never install.

Meshery Broker

SettingWire pathPropagates toObservable as
Versionbroker.versionBroker CR spec.versionBroker version in the header status chip
Replicasbroker.replicasBroker CR spec.sizeNATS statefulset replica count
Service typebroker.service.typeBroker CR spec.service.typemeshery-broker Service type; broker endpoint
Service annotationsbroker.service.annotationsBroker CR spec.service.annotationsAnnotations on the broker client Service
Load balancer classbroker.service.loadBalancerClassBroker CR spec.service.loadBalancerClassWhich LB controller provisions the Service
Load balancer source rangesbroker.service.loadBalancerSourceRangesBroker CR spec.service.loadBalancerSourceRangesWhich CIDRs may reach the broker
External endpoint overridebroker.service.externalEndpointOverrideBroker CR spec.service.externalEndpointOverrideThe advertised broker endpoint (connection diagnostics)

loadBalancerClass and loadBalancerSourceRanges are valid only when service.type is LoadBalancer; the server rejects them otherwise, and the form clears them when the effective type changes away from LoadBalancer. Service changes reconcile in place and must not restart broker pods.

What each deployment mode can apply

Meshery Operator manages MeshSync and Meshery Broker, so the deployment mode is not one setting among many - it decides which of the others can reach anything. In embedded mode Meshery Server runs MeshSync in-process and installs nothing into the cluster: no operator release, no MeshSync Deployment, no Broker, no meshery.io custom resources.

Settingoperatorembedded
operator.deploymentModeappliesapplies
operator.versionappliesinert - no operator release is installed
meshsync.outputNamespaces, meshsync.outputResourcesappliesapplies - passed to the in-process libmeshsync run
meshsync.version, meshsync.replicas, meshsync.watchListappliesinert - MeshSync CR absent
meshsync.redactSecrets, meshsync.brokerContentDedup, meshsync.debugLoggingappliesinert - these are env on the MeshSync Deployment; embedded MeshSync takes them from the Meshery Server process environment
every broker.*appliesinert - no Broker on the cluster

The server states this at apply time through ControllersConfigApplyResult.Skipped. The UI states it before the save: ui/components/configuration/deploymentMode.ts is the single client-side statement of the same structure (takesEffectIn), and the editors are governed by it - the per-connection editor renders a setting the effective mode cannot apply as inert, marks it Not applied, and says why in the form body rather than in a tooltip. Values already stored for such a setting are kept and shown as dormant with a control to clear them; they become live again if the connection moves to operator mode.

The server-wide defaults editor annotates instead of disabling: its mode is only what inheriting connections get, and a connection that overrides the mode to operator uses every value stored there.

That decision needs the mode the connection actually runs, which is why GET /api/integrations/connections/{connectionId}/controllers/config resolves effective.operator.deploymentMode through connections.ResolveConnectionControllersConfig rather than by merging the two editable layers: a connection whose mode comes from the materialized cache or from MESHSYNC_DEFAULT_DEPLOYMENT_MODE would otherwise be described as running the built-in embedded mode, and every applicability decision made from that value would be wrong. merged is deliberately left untouched - it is what propagates to the cluster and must keep carrying only explicitly-set fields.

Server-side apply and withdrawal

Every cluster write uses server-side apply under the meshery-server field manager (server/models/controllers_config_apply.go). The applied document always describes the complete set of fields Meshery Server owns, so:

  • Setting a field takes ownership of it.
  • Clearing it at every layer withdraws it on the next apply, reverting to the operator’s or chart’s own value.

The operator applies the same objects under its own field manager and never sets these env names, args, or the restart annotation, so ownership stays disjoint.

Absent custom resources or Deployment (operator not deployed yet, embedded-mode cluster) are skipped and reported in ControllersConfigApplyResult.Skipped, not treated as errors: the configuration re-applies when the connection reconnects.

Non-layered settings that govern the same components

These are not part of controllers_config but govern the same three components. Contributors changing controller behavior must account for them.

SettingWhereStorageNotes
MESHSYNC_DEFAULT_DEPLOYMENT_MODEServer env / viperProcess configFallback when no layer and no materialization sets a deployment mode. Logged at boot.
MeshSync deployment mode (wizard)Connection Wizard -> MeshSync Mode stepcontrollers_config.operator.deploymentModeReconfigure-time picker. Writes the override through connections.SetDeploymentModeOverride via POST /api/integrations/connections/{id}/actions (setMeshsyncMode).
MeshSync deployment mode (per-context, at import)Connection Wizard -> kubeconfig context reviewSame field, per contextWritten only when the user actually picks one; otherwise the connection inherits.
Operator statusGraphQL changeOperatorStatusCluster stateDeploys/undeploys the operator directly, bypassing the layered document.

One store for the deployment mode

connection.metadata.meshsync_deployment_mode used to carry two different facts under one key - the user’s explicit per-connection choice and a materialized cache of the resolved mode - and that ambiguity was the root cause of two separate defects. The two are now separated (server/models/connections/deployment_mode_resolution.go):

  • controllers_config.operator.deploymentMode is the only store of the explicit choice. Every entry point - the wizard’s MeshSync Mode step, the kubeconfig import picker, the controllers editor - writes it through connections.SetDeploymentModeOverride, so no two controls can disagree about what a connection is set to.
  • meshsync_deployment_mode is only the materialization of the resolved mode, written through connections.MaterializeMeshsyncDeploymentMode and read by the consumers that predate the layered document (the connection state machine, the header status chips, the kubeconfig flows).

connections.ResolveDeploymentMode is the single decision point, and it reports the layer it resolved from:

PrecedenceLayerReported as
1 (highest)Layered document (per-connection override over server-wide default)layeredConfig
2Materialized meshsync_deployment_modelegacyConnectionMetadata
3MESHSYNC_DEFAULT_DEPLOYMENT_MODEserverEnvDefault
4 (lowest)Compiled-in default (embedded)builtIn

Ranking the materialization below the layered document is what lets a server-wide default reach an existing connection: every Kubernetes connection has that key written at registration, so while it sat on top no fan-out could ever change a mode. It survives as a compatibility floor for connections registered before the layered document existed - their recorded mode is kept when, and only when, no layer sets one.

A server-wide default change is not just a document to re-apply. reconcileInheritedDeploymentMode persists the refreshed materialization and drives the same undeploy/redeploy path the wizard uses, so an inheriting connection actually switches modes on the cluster.

API

MethodPathHandler
GET/api/system/controllers/configGetControllersDefaultConfig
PUT/api/system/controllers/configUpdateControllersDefaultConfig
GET/api/integrations/connections/{connectionId}/controllers/configGetConnectionControllersConfig
PUT/api/integrations/connections/{connectionId}/controllers/configUpdateConnectionControllersConfig

Both PUTs validate before persisting (connections.ValidateControllersConfig) and return the stored document. The connection PUT additionally applies the resolved configuration to that connection’s cluster; the system PUT fans the re-apply out to every tracked Kubernetes connection.

Writes are asynchronous with respect to the cluster: the override persists even if the apply fails, and the failure surfaces as an event rather than an HTTP error, because the configuration re-applies on the next connect.

Known gaps

Tracked here so they are not rediscovered. Each is a defect, not a design.

  1. The apply result is invisible. ApplyControllersConfigToCluster returns which custom resources it patched, whether MeshSync was restarted, and a Skipped[] list with human-readable reasons. That result only reaches event metadata; both editors report “saved” either way, so a save that changed nothing on the cluster looks identical to one that changed everything.

Test coverage

Unit coverage that exists today:

  • server/models/connections/controllers_config_test.go - precedence, atomic collection merge, metadata round-trip, validation, and the single reader of operator.version.
  • server/models/connections/deployment_mode_resolution_test.go - deployment mode precedence (a server-wide default reaching an inheriting connection), the wizard/editor convergence on one store, and the effective document reporting the mode a connection actually runs.
  • server/models/operator_chart_version_test.go - operator.version selecting the operator Helm chart version, the layering it resolves through, and the cases where a chart-version reconcile must not touch the cluster.
  • ui/components/configuration/__tests__/deploymentMode.test.ts - which settings each deployment mode can apply, and how the mode governing each editor is resolved and attributed to a layer.
  • ui/components/configuration/__tests__/ControllersConfigForm.test.tsx - the rendered editor: tri-state inherit/override, the conditional LoadBalancer-only fields, and the deployment-mode gating (inert-and-explained on a connection, annotated-but-live on the server-wide defaults).
  • server/models/controllers_config_apply_test.go - cluster propagation with fake clients: per-setting custom-resource and Deployment patch contents, withdrawal when a field is cleared at every layer, restart-only-on-watch-list-change, and the Skipped[] reporting.
  • server/handlers/controllers_config_handlers_test.go - the four endpoints: validation rejection, the layered response shape, clearing an override, and that an unreadable override is skipped rather than reconciled.

End-to-end coverage lives in ui/tests/e2e/controllers-config.spec.ts, keyed to the Operator, MeshSync & Broker Settings Test Plan Test Group via the Allure testGroup label - the same mechanism the Connection Lifecycle report uses (see Contributing to Meshery’s CLI tests). It covers every wire path in the tables above plus the precedence chain, the inherit round-trip, validation rejection, and the mode gating.

What remains uncovered:

  • Cluster propagation end to end. The spec’s watch-scope and broker-service case self-skips when no Kubernetes cluster is reachable, so on an infra-less run it reports “skipped” rather than passing vacuously. The propagation logic itself is covered by the unit test above; what is untested is the real round trip against a live operator.

Before adding a setting, add its row above, its propagation assertion, and its end-to-end case. A setting whose effect a user cannot observe is a defect; so is one that fails silently.