Skip to content

Manage Solr's replica placement plugin - #894

Open
janhoy wants to merge 23 commits into
apache:mainfrom
janhoy:issue-691-replica-placement-factory
Open

janhoy wants to merge 23 commits into
apache:mainfrom
janhoy:issue-691-replica-placement-factory

Conversation

@janhoy

@janhoy janhoy commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

🤖 AI assisted 🤖

Closes #691

Stacked on #827, which this needs for Solr version detection. Review that first. Only the last commit belongs to this PR.

Problem. Solr picks replica placement with a cluster-wide plugin and defaults to SimplePlacementFactory. To use anything else, including AffinityPlacementFactory, you either maintain a full custom solr.xml or POST to /api/cluster/plugin by hand after every cluster rebuild. Neither is declarative, and the custom solr.xml route costs you the operator-generated solr.xml entirely.

Solution. A new SolrCloud section:

spec:
  replicaPlacementFactory:
    class: org.apache.solr.cluster.placement.plugins.AffinityPlacementFactory
    config:
      minimalFreeDiskGB: 10
      prioritizedFreeDiskGB: 200

The operator applies this through /api/cluster/plugin once the cloud has a ready pod, under the reserved name .placement-plugin. Removing the section removes the plugin. Changes need no pod restart. Also exposed as the Helm value solrOptions.replicaPlacementFactory.

Why the API and not solr.xml. The issue proposed rendering <replicaPlacementFactory> into the generated solr.xml. Two findings moved me off that:

  • Declaring any cluster plugin in solr.xml requires solr.cluster.plugin.edit.enabled=false, or NodeConfig throws at startup and no node comes up. That property then disables the /cluster/plugin edit API cluster-wide, for cluster singletons and custom plugins too. The operator would be imposing a cluster-wide restriction to deliver one feature.
  • AffinityPlacementConfig has long, Integer, Boolean and three Map<String,String> fields, so solr.xml would need correct type tags plus nested <lst> rendering. Over the API the config is JSON and passes through untouched.

Design notes.

  • config is arbitrary JSON via x-kubernetes-preserve-unknown-fields, so any plugin's options work with no per-option plumbing. The tradeoff is no schema validation of keys.
  • Re-application is driven by a hash of the desired plugin in status.appliedReplicaPlacementFactory, so Solr is called only when the spec changes. The hash normalises the JSON first, so reformatting the config does not trigger a reapply. A cloud configuring no plugin never calls Solr at all.
  • Solr 9+ is required, gated with SolrVersionAtLeast from Add Solr 10 compatibility support #827. On Solr 8 the setting is ignored with a warning event rather than retried.
  • No security.json change needed. The endpoint requires COLL_EDIT_PERM, which the operator's k8s role already holds through collection-admin-edit.

Also fixes a latent panic in CallCollectionsApiV2. It passed a nil *bytes.Buffer as the request body, which is a non-nil io.Reader wrapping a nil pointer, so http.NewRequestWithContext panicked calling Len() on it. No previous caller passed a nil body, so this GET was the first to hit it. The panic aborted the whole reconcile, leaving the SolrCloud stuck at zero ready replicas in its status. Present on main, independent of this feature, and easy to split out if preferred.

Testing. Five unit tests cover plugin construction, JSON passthrough including nested objects and non-string types, hash stability against reformatting, and rejection of invalid JSON. The full unit suite passes, as do check-manifests, check-generated, check-mod, check-format, vet and check-helm.

Manually verified on a kind cluster with Solr 9.11 and the e2e harness:

  • the config reaches Solr intact, integers and booleans included
  • changing the config updates the existing plugin, confirmed by GET /api/cluster/plugin
  • removing the section removes the plugin and clears the status hash
  • a Solr 8.11 cloud gets the ReplicaPlacementPluginUnsupported warning and no retry loop
  • five forced reconciles produce no additional calls to Solr

That manual run is also what surfaced both bugs above, so the API call is still worth an e2e spec. I have not added one.

danielmeint and others added 21 commits April 27, 2026 21:46
Solr 10 introduced several breaking changes that prevent the operator
from successfully starting and managing SolrClouds. This change adds
version-conditional behavior for Solr 10 while preserving full backwards
compatibility with Solr 9.x.

Major changes covered:

* solr.xml — Solr 10 removed several `<solrcloud>` parameters
  (genericCoreNodeNames, hostContext, allowPaths, metricsEnabled).
  A new `DefaultSolrXMLForSolr10` template, mirroring the stock Solr 10
  template, is selected when the image tag indicates Solr 10+.

* Host advertise — the `host` system property was renamed to
  `solr.host.advertise`. A `SOLR_HOST_ADVERTISE` env var is now set on
  Solr 10 pods.

* Modules — Solr 10 removed `/opt/solr/contrib/<module>/lib` and
  `/opt/solr/dist`. Modules are now loaded via the `SOLR_MODULES` env
  var, and the operator no longer emits contrib paths in `sharedLib`
  for Solr 10.

* hostPort sysprop — `-DhostPort` is no longer needed in Solr 10 and
  is skipped.

* zkcli.sh removed — `setUrlSchemeClusterPropCmd` (TLS setup) now uses
  `solr cluster --property` and `solr zk cp` for Solr 10 instead of
  the removed `cloud-scripts/zkcli.sh`.

* `solr api` CLI — `-get URL` was replaced with `--solr-url URL`.
  Secure probes (`useSecureProbe`) and the e2e helper
  (`callSolrApiInPod`) emit the new flag for Solr 10.

* Basic auth — Solr 10 no longer honors the deprecated
  `-Dbasicauth=user:pass` JAVA_TOOL_OPTIONS path. The e2e helper now
  uses the native `--credentials user:pass` flag for Solr 10.

Version detection lives on the `SolrCloud` type as
`(*SolrCloud).IsSolr10OrLater()`, which parses the major version from
the image tag and treats unparseable tags (e.g. "latest", "nightly")
and a nil `SolrImage` as pre-10 for backwards compatibility. A
package-level `IsSolr10OrLater(imageTag string)` is also exported for
callers that only have a raw image string.

Unit tests cover the version parser, both `solr.xml` templates,
`setUrlSchemeClusterPropCmd` for both branches, the env vars emitted
by `GenerateStatefulSet` for Solr 9 vs Solr 10, and both branches of
`useSecureProbe`. End-to-end tests have been verified against
Solr 10.0.0 across Basic, Scaling (with replica migration),
Security JSON (provided + bootstrapped), TLS (Secrets and Mounted Dir,
including ClientAuth Need/Want, CheckPeerName, VerifyClientHostname),
Local-directory backups (recurring + single), Ingress, and Managed
Rolling Upgrades. Solr 9.8.0 Basic was verified as a regression
baseline.

The Prometheus exporter is not covered: `solr-exporter` was removed
from the Solr distribution in 10, and metrics are now expected to be
scraped from Solr's built-in endpoint. That work is tracked separately
in apache#820.

Refs apache#821, apache#826.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove SolrMajorVersion(tag) and standalone IsSolr10OrLater(tag) functions.
Version detection is now driven entirely by the required solrMajorVersion
CRD field (validated 8–10), eliminating misdetection of custom image tags
like '261.162.1' as Solr 261.
Adds a new `solrMajorVersion` value (default 9, matching the default
image.tag of 9.10.0) and renders it under spec.solrMajorVersion. Uses
Helm's `required` so the chart fails fast with a clear message if a
user explicitly unsets it. Documents the new value in the chart README
and adds a v0.10.0 upgrade note covering both the CRD field and the
new chart value.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ction

Drops "Required by the operator" from the chart README row since the
table already shows a default of 9, which reads as contradictory at
the chart layer. The CRD field is still required at the API layer;
the chart just hands it a default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reframes the solrMajorVersion entry to make the CRD-required vs
chart-defaulted distinction explicit (raised in PR apache#827 review).
Adds two new entries surfaced by PR apache#827 testing:

* SolrPrometheusExporter must be removed before bumping a SolrCloud
  to Solr 10.x, otherwise the rolling upgrade stalls and requires
  rollback (apache#820).
* On Solr 10 the operator sets SOLR_MODULES on pods (since contrib
  paths no longer exist in the image). Modules baked into a custom
  image's Dockerfile via SOLR_MODULES are overridden after upgrade;
  declare modules via spec.solrModules instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* helm/solr-operator/Chart.yaml: append an `added` entry under the
  existing v0.10.0 changelog covering the new required
  spec.solrMajorVersion field and Solr 10 support.
* helm/solr/Chart.yaml: replace the placeholder entries with one
  `added` entry covering the new solrMajorVersion chart value.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two CI fixes for the new required SolrMajorVersion CRD field:

* gofmt: struct field alignment in tests/e2e/test_utils_test.go and
  controllers/util/solr_util_test.go broke when SolrMajorVersion was
  added to several SolrCloudSpec literals. Reformatted via make fmt.

* Controller tests: CRD validation now rejects SolrCloud resources
  that omit spec.solrMajorVersion, which caused 61/71 specs in the
  smoke-test suite to fail at [JustBeforeEach]. Default the field to
  9 in each JustBeforeEach (only when unset, so tests can still
  override) and add SolrMajorVersion: 9 to the two inline solrRef
  literals in the SolrPrometheusExporter tests.

Verified locally with make lint, make test, and make build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts:
#	docs/upgrade-notes.md
#	helm/solr-operator/Chart.yaml
#	helm/solr/Chart.yaml
Replaces the required spec.solrMajorVersion field with parsing the Solr
major version from spec.solrImage.tag, as agreed in the PR discussion.
The tag must start with the Solr version (e.g. 10.0.0, 9.10.0-slim, 9.10,
8.11, 10); only majors 8 to 10 are accepted. SolrClouds with other tags
(latest, custom tags, unsupported majors) get an UnsupportedSolrVersion
Warning event and are not reconciled, but can still be deleted. Short
versions are accepted because released operator versions stored their
default tag (8.11) in the SolrCloud spec. The CRDs and the solr Helm
chart are unchanged compared to main.

Further Solr 10 fixes:
- set SOLR_PORT_LISTEN, which Solr 10's bin/solr reads instead of SOLR_PORT
- keep -DhostPort, for custom solr.xml files that still use ${hostPort:}
- load modules from /opt/solr/modules/<module>/lib via sharedLib instead
  of SOLR_MODULES, the same mechanism as for Solr 9
- align the Solr 10 solr.xml with Solr 10's stock solr.xml (enforced by a test)
- pass probe credentials via --credentials, -Dbasicauth via
  JAVA_TOOL_OPTIONS no longer works on Solr 10
- show the right version in the node status for digest-pinned tags

Tests: version parsing, reconcile skip and deletion with an unsupported
tag, probe auth on Solr 9 and 10, custom podPort, Solr 10 module paths,
an e2e case for probesRequireAuth, exporter e2e tests skipped on Solr 10.
Docs: Solr 10 column in the version matrix, image tag rules, upgrade
notes for tags, custom solr.xml and the Prometheus exporter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Compare node versions with the SolrCloud's tag without a digest, so a
  tag like 10.0.0@sha256:... no longer leaves the SolrCloud looking stuck
  mid-upgrade (TargetVersion never cleared)
- Correct the comment on why Solr 10 probes use --credentials
- Docs: Solr 10 note on the Prometheus exporter page, tag rule in the
  solr chart README, upgrade notes on the metrics element and on indexes
  created with Solr 8

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- No-restart claim
- Custom log4j2.xml and solr.logs.dir
# Conflicts:
#	docs/modules/upgrade-notes/pages/upgrade-notes.adoc
#	helm/solr-operator/Chart.yaml
#	helm/solr/README.md
- Drop the metricsEnabled upgrade note; Solr 10 still supports the metrics element
- Mention SOLR_ALLOW_PATHS, which works unchanged on Solr 9.6+ and Solr 10
- Shorten the image.tag row in the solr chart README
- Add the image tag requirement to the solr chart changelog
- Use a version-agnostic image tag in the SolrPrometheusExporter tests
Version checks are now inlined at each conditional instead of being cached
in per-function isSolr10 variables.
Solr 10 removed the Prometheus exporter, so such a Deployment can only crash-loop.
Emit an UnsupportedSolrVersion Warning event and log an error instead.
Tags that don't start with a Solr version are still allowed for custom exporter images.
…gs on Solr 10

- Don't reconcile a SolrPrometheusExporter that refers to a Solr 10 SolrCloud either,
  as Solr 10 only serves metrics in Prometheus format, and block Solr 11+ images too
- Check before creating any exporter resources, and mark the exporter not ready
- Warn (without blocking) when a Solr 10 cloud's custom solr.xml contains
  hostContext or genericCoreNodeNames
- Upgrade notes: SOLR_SECURITY_ALLOW_PATHS on Solr 10, as SOLR_ALLOW_PATHS is deprecated there
# Conflicts:
#	.github/workflows/e2e-tests.yaml
#	helm/solr-operator/Chart.yaml
The merge with main took main's e2e matrix, which drives solr-version from
the workflow_dispatch input and so tests a single version. Restore the Solr
10 dimension this branch had, keeping main's input-driven manual runs: a
pull request tests 9.11.0 and 10.1.0, a manual run tests only the version
given by the input.

fail-fast is off so that a failure on one Solr version does not cancel the
job for the other.
New SolrCloud option `replicaPlacementFactory` sets the cluster-wide
replica placement plugin and its config. The operator applies it through
Solr's /api/cluster/plugin API once the cloud has a ready pod, and
re-applies it when the section changes, tracked by a hash in the status.
Removing the section removes the plugin.

The config is passed to Solr as arbitrary JSON, so it covers any
plugin's settings including the map-valued ones, with no per-option
plumbing. Requires Solr 9+, which is gated using the version detection
from the Solr 10 compatibility work this builds on.

Also fixes a latent panic in CallCollectionsApiV2: a nil *bytes.Buffer
was passed as the request body, which is a non-nil io.Reader that
http.NewRequestWithContext panics on. No existing caller passed a nil
body, so this is the first call to hit it.

Closes apache#691

This comment was marked as resolved.

- Keep JSON numbers as their original literal when normalising the
  config, so a value a float64 cannot hold exactly is neither corrupted
  on its way to Solr nor collapsed into the same hash as its neighbour.
  Trailing data after the JSON value is still rejected.
- Include the ZooKeeper ensemble and chroot in the applied hash. Cluster
  plugins live in ZooKeeper, so pointing a SolrCloud at a fresh target
  has to re-apply the plugin rather than trust the old hash.
- Gate the removal path on the Solr version too. A cloud rolled back to
  an unsupported version with the section removed was calling the
  missing API every 15 seconds; the hash is now retained until a
  supported version can remove the plugin.

Four regression tests added, one per behaviour.
@janhoy
janhoy marked this pull request as ready for review November 9, 2026 09:18
// last applied to the cluster, used to detect when it needs to be re-applied. Empty when no
// placement plugin is managed by the operator.
// +optional
AppliedReplicaPlacementFactory string `json:"appliedReplicaPlacementFactory,omitempty"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we instead keep this in the statefulSet annotations, like we do for the other hashes? (I do know they mostly live in the pod annotations, where this would live in the statefulset annotations because we don't want a restart).\

Maybe instead we have a section for Status.appliedSettings and have replicaPlacementFactory as a key in there? Because I imagine that we could have other settings added in the future. Though in the future, hopefully Solr has a Kubernetes plugin that will allow this stuff to just live in Kubernetes configMaps instead of zookeeper...

This branch has not been deployed

No deployments
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.

Support replicaPlacementFactory in solr.xml

4 participants