-
Notifications
You must be signed in to change notification settings - Fork 807
SOLR-18079: Allow user to provide custom Implicit Plugins Mapping File #4067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
be6f3eb
342d5c7
11cee64
c47060d
b4633ff
bbc6e21
3a11769
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3614,32 +3614,114 @@ public void cleanupOldIndexDirectories(boolean reload) { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Parses implicit plugin definitions from a JSON map and converts them to PluginInfo objects. | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * @param implicitPluginsInfo the parsed JSON map containing plugin definitions | ||||||||||||||||||||||
| * @return an unmodifiable list of PluginInfo objects for request handlers | ||||||||||||||||||||||
| * @throws NullPointerException if requestHandlers section is missing | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| static List<PluginInfo> parseImplicitPlugins(Map<String, ?> implicitPluginsInfo) { | ||||||||||||||||||||||
| @SuppressWarnings("unchecked") | ||||||||||||||||||||||
| Map<String, Map<String, Object>> requestHandlers = | ||||||||||||||||||||||
| (Map<String, Map<String, Object>>) implicitPluginsInfo.get(SolrRequestHandler.TYPE); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (requestHandlers == null) { | ||||||||||||||||||||||
| throw new IllegalArgumentException("No requestHandler section found in implicit plugins"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| List<PluginInfo> implicits = new ArrayList<>(requestHandlers.size()); | ||||||||||||||||||||||
| for (Map.Entry<String, Map<String, Object>> entry : requestHandlers.entrySet()) { | ||||||||||||||||||||||
| Map<String, Object> info = entry.getValue(); | ||||||||||||||||||||||
| info.put(CommonParams.NAME, entry.getKey()); | ||||||||||||||||||||||
| implicits.add(new PluginInfo(SolrRequestHandler.TYPE, info)); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return Collections.unmodifiableList(implicits); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static final class ImplicitHolder { | ||||||||||||||||||||||
| private ImplicitHolder() {} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static final List<PluginInfo> INSTANCE; | ||||||||||||||||||||||
| private static volatile List<PluginInfo> INSTANCE = null; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| static List<PluginInfo> getInstance(SolrCore core) { | ||||||||||||||||||||||
| if (INSTANCE == null) { | ||||||||||||||||||||||
| synchronized (ImplicitHolder.class) { | ||||||||||||||||||||||
| if (INSTANCE == null) { | ||||||||||||||||||||||
| INSTANCE = loadImplicitPlugins(core); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return INSTANCE; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static List<PluginInfo> loadImplicitPlugins(SolrCore core) { | ||||||||||||||||||||||
| // Check for custom implicit plugins file from solr.xml (global configuration) | ||||||||||||||||||||||
| String customPluginsFile = core.getCoreContainer().getConfig().getImplicitPluginsFile(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (customPluginsFile != null && !customPluginsFile.isEmpty()) { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| // Resolve path similar to solr.xml - support both absolute and relative paths | ||||||||||||||||||||||
| Path customPluginsPath = Path.of(customPluginsFile); | ||||||||||||||||||||||
| core.getCoreContainer().assertPathAllowed(customPluginsPath); | ||||||||||||||||||||||
| if (!customPluginsPath.isAbsolute()) { | ||||||||||||||||||||||
| // Resolve relative paths against SOLR_HOME | ||||||||||||||||||||||
| Path solrHome = core.getCoreContainer().getSolrHome(); | ||||||||||||||||||||||
| customPluginsPath = solrHome.resolve(customPluginsFile); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
||||||||||||||||||||||
| // Ensure the resolved path is within allowed locations | |
| core.getCoreContainer().assertPathAllowed(customPluginsPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good find by copilot! (The fact that assertPathAllowed is not called before using the string)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, heck, I went and fussed aroudn for a while and missed this fix, which was what i did eventually!
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message uses the string representation of the customPluginsFile variable instead of the actual path that was attempted to be loaded (customPluginsPath). This makes debugging harder since the user sees the input value, not the resolved path that was actually checked. Consider using customPluginsPath.toString() in the log messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe, but we don't actually ahve this available to us in the catch
Copilot
AI
Jan 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message uses the string representation of the customPluginsFile variable instead of the actual path that was attempted to be loaded (customPluginsPath). This makes debugging harder since the user sees the input value, not the resolved path that was actually checked. Consider using customPluginsPath.toString() in the log messages.
| customPluginsFile); | |
| } catch (Exception e) { | |
| log.warn( | |
| "Failed to load custom implicit plugins file: {} (from solr.xml). Falling back to default.", | |
| customPluginsFile, | |
| customPluginsPath.toString()); | |
| } catch (Exception e) { | |
| log.warn( | |
| "Failed to load custom implicit plugins file: {} (from solr.xml). Falling back to default.", | |
| customPluginsPath.toString(), |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "requestHandler": { | ||
| "/custom-update": { | ||
| "class": "solr.UpdateRequestHandler", | ||
| "defaults": { | ||
| "custom": "true" | ||
| } | ||
| }, | ||
| "/custom-select": { | ||
| "class": "solr.SearchHandler", | ||
| "defaults": { | ||
| "echoParams": "all" | ||
| } | ||
| }, | ||
| "/admin/ping": { | ||
| "class": "solr.PingRequestHandler", | ||
| "invariants": { | ||
| "echoParams": "all", | ||
| "q": "{!lucene}*:*" | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,10 @@ | |
| import io.opentelemetry.exporter.prometheus.PrometheusMetricReader; | ||
| import io.prometheus.metrics.model.snapshots.GaugeSnapshot; | ||
| import io.prometheus.metrics.model.snapshots.MetricSnapshots; | ||
| import java.io.InputStream; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.Paths; | ||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
|
|
@@ -32,6 +36,7 @@ | |
| import org.apache.solr.common.SolrException; | ||
| import org.apache.solr.common.util.ExecutorUtil; | ||
| import org.apache.solr.common.util.SolrNamedThreadFactory; | ||
| import org.apache.solr.common.util.Utils; | ||
| import org.apache.solr.handler.ReplicationHandler; | ||
| import org.apache.solr.handler.RequestHandlerBase; | ||
| import org.apache.solr.handler.component.QueryComponent; | ||
|
|
@@ -146,6 +151,62 @@ public void testImplicitPlugins() { | |
| assertEquals("wrong number of implicit handlers", ihCount, implicitHandlers.size()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testCustomImplicitPlugins() throws Exception { | ||
| // Test that the custom implicit plugins file can be loaded and parsed into PluginInfo objects | ||
| String customPluginsPath = TEST_HOME() + "/custom-implicit-plugins.json"; | ||
| Path pluginsFile = Paths.get(customPluginsPath); | ||
|
|
||
| // Verify file exists | ||
| assertTrue( | ||
| "Custom implicit plugins file should exist: " + customPluginsPath, | ||
| Files.exists(pluginsFile)); | ||
|
|
||
| // Load and parse the custom plugins file | ||
| try (InputStream is = Files.newInputStream(pluginsFile)) { | ||
| @SuppressWarnings("unchecked") | ||
| Map<String, ?> implicitPluginsInfo = (Map<String, ?>) Utils.fromJSON(is); | ||
|
|
||
| assertNotNull("Should parse custom plugins JSON", implicitPluginsInfo); | ||
|
|
||
| // Call parseImplicitPlugins to convert JSON to PluginInfo objects | ||
| List<PluginInfo> customHandlers = SolrCore.parseImplicitPlugins(implicitPluginsInfo); | ||
|
|
||
| assertNotNull("Should return list of PluginInfo objects", customHandlers); | ||
| assertEquals("Should have 3 custom handlers", 3, customHandlers.size()); | ||
|
|
||
| // Build a map for easy verification | ||
| Map<String, String> pathToClassMap = new HashMap<>(customHandlers.size()); | ||
| for (PluginInfo handler : customHandlers) { | ||
| assertEquals( | ||
| "All handlers should be of type requestHandler", SolrRequestHandler.TYPE, handler.type); | ||
| pathToClassMap.put(handler.name, handler.className); | ||
| } | ||
|
|
||
| // Verify custom handlers are present with correct classes | ||
| assertEquals( | ||
| "Custom update handler should be UpdateRequestHandler", | ||
| "solr.UpdateRequestHandler", | ||
| pathToClassMap.get("/custom-update")); | ||
| assertEquals( | ||
| "Custom select handler should be SearchHandler", | ||
| "solr.SearchHandler", | ||
| pathToClassMap.get("/custom-select")); | ||
| assertEquals( | ||
| "Custom ping handler should be PingRequestHandler", | ||
| "solr.PingRequestHandler", | ||
| pathToClassMap.get("/admin/ping")); | ||
|
|
||
| // Verify default handlers are NOT present (since we're using custom file) | ||
| assertNull( | ||
| "Default /debug/dump should not be present with custom plugins", | ||
| pathToClassMap.get("/debug/dump")); | ||
| assertNull( | ||
| "Default /update should not be present with custom plugins", | ||
| pathToClassMap.get("/update")); | ||
| } | ||
| } | ||
|
Comment on lines
+154
to
+208
|
||
|
|
||
| @Test | ||
| public void testClose() { | ||
| final CoreContainer cores = h.getCoreContainer(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ | |
| <str name="allowUrls">${solr.security.allow.urls:}</str> | ||
| <str name="hideStackTrace">${solr.hideStackTrace:false}</str> | ||
| <int name="indexSearcherExecutorThreads">${solr.searchThreads:0}</int> | ||
| <str name="implicitPluginsFile">${solr.implicitPluginsFile:}</str> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you consider a "modern" property name?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really because all the other ones are the same. So kept the existing style. In another PR I'd love to get these all renamed! |
||
|
|
||
| <solrcloud> | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wasn't it suggested to disallow path.resolve due to how it will resolve
../../..patterns perhaps unexpectedly? However, if callingassertPathAllowedbefore use, we should be safe anywayThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't remember how to do the assert! We need it in our prompt.md ;-)