-
Notifications
You must be signed in to change notification settings - Fork 819
SOLR-15701: Complete configsets api #4264
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
a5c6ccc
8dc9e80
f2c1d8f
c7f7ad4
c6b4d9c
e77c73e
be4717b
497b5ba
1cf5545
2c6e7bc
f200359
9997a9a
6d2aac9
e59a2d2
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 |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| title: Add ConfigSets.Download and ConfigSets.GetFile to SolrJ | ||
| type: added | ||
| authors: | ||
| - name: Eric Pugh | ||
| links: | ||
| - name: SOLR-15701 | ||
| url: https://issues.apache.org/jira/browse/SOLR-15701 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,15 +16,23 @@ | |
| */ | ||
| package org.apache.solr.client.api.endpoint; | ||
|
|
||
| import static org.apache.solr.client.api.util.Constants.RAW_OUTPUT_PROPERTY; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.extensions.Extension; | ||
| import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; | ||
| import io.swagger.v3.oas.annotations.parameters.RequestBody; | ||
| import jakarta.ws.rs.DELETE; | ||
| import jakarta.ws.rs.GET; | ||
| import jakarta.ws.rs.POST; | ||
| import jakarta.ws.rs.PUT; | ||
| import jakarta.ws.rs.Path; | ||
| import jakarta.ws.rs.PathParam; | ||
| import jakarta.ws.rs.Produces; | ||
| import jakarta.ws.rs.QueryParam; | ||
| import jakarta.ws.rs.core.MediaType; | ||
| import jakarta.ws.rs.core.Response; | ||
| import jakarta.ws.rs.core.StreamingOutput; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import org.apache.solr.client.api.model.CloneConfigsetRequestBody; | ||
|
|
@@ -71,30 +79,75 @@ SolrJerseyResponse deleteConfigSet(@PathParam("configSetName") String configSetN | |
| throws Exception; | ||
| } | ||
|
|
||
| /** V2 API definition for downloading an existing configset as a ZIP archive. */ | ||
| @Path("/configsets/{configSetName}") | ||
| interface Download { | ||
| @GET | ||
| @Path("/download") | ||
| @Operation( | ||
| summary = "Download a configset as a ZIP archive.", | ||
| tags = {"configsets"}, | ||
| extensions = { | ||
| @Extension(properties = {@ExtensionProperty(name = RAW_OUTPUT_PROPERTY, value = "true")}) | ||
| }) | ||
| @Produces("application/zip") | ||
| Response downloadConfigSet(@PathParam("configSetName") String configSetName) throws Exception; | ||
| } | ||
|
|
||
| /** | ||
| * V2 API definitions for uploading a configset, in whole or part. | ||
| * V2 API definition for reading a single file from an existing configset. | ||
| * | ||
| * <p>Returns the raw bytes of the file, suitable for both text and binary files. | ||
| * | ||
| * <p>Equivalent to GET /api/configsets/{configSetName}/files/{filePath} | ||
| */ | ||
| @Path("/configsets/{configSetName}") | ||
| interface GetFile { | ||
| @GET | ||
| @Path("/files/{filePath:.+}") | ||
| @Produces(MediaType.APPLICATION_OCTET_STREAM) | ||
| @Operation( | ||
| summary = "Get the raw contents of a file in a configset.", | ||
| tags = {"configsets"}, | ||
| extensions = { | ||
| @Extension(properties = {@ExtensionProperty(name = RAW_OUTPUT_PROPERTY, value = "true")}) | ||
| }) | ||
| StreamingOutput getConfigSetFile( | ||
| @PathParam("configSetName") String configSetName, @PathParam("filePath") String filePath) | ||
| throws Exception; | ||
| } | ||
|
|
||
| /** | ||
| * V2 API definition for uploading an entire configset as a ZIP archive. | ||
| * | ||
| * <p>Equivalent to the existing v1 API /admin/configs?action=UPLOAD | ||
| */ | ||
| @Path("/configsets/{configSetName}") | ||
| interface Upload { | ||
| @PUT | ||
| @Operation(summary = "Create a new configset.", tags = "configsets") | ||
| @Operation(summary = "Upload a configset as a ZIP archive.", tags = "configsets") | ||
| SolrJerseyResponse uploadConfigSet( | ||
| @PathParam("configSetName") String configSetName, | ||
| @QueryParam("overwrite") Boolean overwrite, | ||
| @QueryParam("cleanup") Boolean cleanup, | ||
| @RequestBody(required = true) InputStream requestBody) | ||
| throws IOException; | ||
| } | ||
|
|
||
| /** | ||
| * V2 API definition for putting a single file to an existing configset. | ||
| * | ||
| * <p>This endpoint allows updating individual configuration files without re-uploading the entire | ||
| * configset. The file path is specified as part of the URL path. | ||
| */ | ||
| @Path("/configsets/{configSetName}") | ||
| interface PutFile { | ||
| @PUT | ||
| @Path("{filePath:.+}") | ||
|
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. [-0] We should probably add |
||
| @Operation(summary = "Create a new configset.", tags = "configsets") | ||
| SolrJerseyResponse uploadConfigSetFile( | ||
| @Operation(summary = "Upload a single file to a configset.", tags = "configsets") | ||
| SolrJerseyResponse putConfigSetFile( | ||
| @PathParam("configSetName") String configSetName, | ||
| @PathParam("filePath") String filePath, | ||
| @QueryParam("overwrite") Boolean overwrite, | ||
|
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. [Q] Can you please add some context on why these boolean flags are going away? 'Overwrite' in particular seems like a nice safety check that I imagine users would like to use. |
||
| @QueryParam("cleanup") Boolean cleanup, | ||
| @RequestBody(required = true) InputStream requestBody) | ||
| throws IOException; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You 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 org.apache.solr.client.api.model; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| /** Response type for the "get configset file contents" API. */ | ||
| public class ConfigSetFileContentsResponse extends SolrJerseyResponse { | ||
|
|
||
| /** The path of the file within the configset (as requested). */ | ||
| @JsonProperty("path") | ||
| public String path; | ||
|
|
||
| /** The UTF-8 text content of the file. */ | ||
| @JsonProperty("content") | ||
| public String content; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,7 +42,7 @@ | |
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * FileSystem ConfigSetService impl. | ||
| * File system based ConfigSetService implementation. | ||
| * | ||
| * <p>Loads a ConfigSet defined by the core's configSet property, looking for a directory named for | ||
| * the configSet property value underneath a base directory. If no configSet property is set, loads | ||
|
|
@@ -182,6 +182,11 @@ public void uploadFileToConfig( | |
| } | ||
|
|
||
| if (overwriteOnExists || !Files.exists(configsetFilePath)) { | ||
| // Create parent directories if they don't exist (similar to ZK's makePath) | ||
|
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. [Q] Why is this code here? This feels kindof suspect at a glance, from a security perspective. The risk is a bit lower in ZK, but since we're on the filesystem here we probably shouldn't blindly be creating directories based on a user-specified parameter. If we're 100% certain that there's no path-escape or any other funny-business possible here, maybe this is fine. But I'd think this through really carefully if you haven't already. |
||
| Path parent = configsetFilePath.getParent(); | ||
| if (parent != null && !Files.exists(parent)) { | ||
| Files.createDirectories(parent); | ||
| } | ||
| Files.write(configsetFilePath, data); | ||
| } | ||
| } | ||
|
|
||
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.
[-1]
/downloadisn't very REST-ful. The fact that we're fetching/downloading the resource is already kindof implied by the HTTP 'GET' verb/method.Could we drop it and make this "just"
GET /api/configsets/{configsetName}?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.
so we could, and I was thinking about it and then asked Claude for soem suggestions, and he kind of came back to "/download" is easy. Here is the write up:
RESTful Alternatives
Option 1: Keep current design ✅ (Recommended)
Pros:
/downloadtells you what it does)/archive, GitLab uses/repository/archive, etc.)Cons:
Option 2: Use
/filessub-resourcePros:
Cons:
Option 3: Content negotiation (Pure REST)
Pros:
Cons:
Acceptheaders this way/{filePath}patternRecommendation
Keep the current design (
/download) because:GET /api/configsets/{name}for JSONThere 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.
If we had another places that we did the zip download, then I could see making them all work the same, with whatever pattern we picked...
Uh oh!
There was an error while loading. Please reload this page.
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.
I don't mean to be glib, but I don't really care what Claude thinks - I care what you think! Maybe by quoting Claude you're implying you agree? But I'd love to hear which option you think is better, and why!
Idk about ZIP downloads, that's admittedly a little more niche 🤷
What is clear to me is that we have a bunch of APIs that let you "download some thing" or "download some part of some thing", and none of those APIs take the
/downloadapproach suggested by your LLM.GET /api/cluster/filestore/files/<path>GET /api/cores/coreName/replication/files/<path>GET /api/cluster/zookeeper/data/<path>We've got an established pattern here: rely on the HTTP verb to imply "download", and offer a "files/" sub-path as a way to support individual file download. If we want to deviate from that there should be a pretty compelling reason.
And while I hear Claude's point that we may want to reserve
/api/configsets/<name>for some future metadata at some point...that doesn't feel very compelling to me. "configset metadata" isn't a thing in Solr today. And as Claude pointed out there are options (e.g.Acceptheader) for adding that pretty seamlessly down the road.To be transparent, if you personally feel strongly about
/downloadI'm not gonna veto the PR or anything. But Claude's argument for/downloaddoesn't make a ton of sense to me, and I think we should be intentional about these endpoints.