Repository navigation
SOLR-18212: Migrate PackageAPI endpoints from @EndPoint/@Command to JAX-RS #4178
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
Changes from all commits
a86cf71
9422c8c
744e8ff
cc26412
5040f5b
83c2f5c
d71f199
8994e03
199b0f3
463f590
ae94dd3
62212c3
30ab7cc
99d90e7
7967574
7491730
deb2142
2578d26
bf46dc7
a0bef90
ba61c1b
670562d
702b181
1cfc294
b51a155
742b0ba
59c1c26
47fd06e
aa2d56e
2445ef1
901d90e
9ff3b2f
8a6bdd2
2690834
f8ed9a0
93a67ec
7503001
afbad43
140191d
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,9 @@ | ||
| title: The package management API now has OpenAPI and SolrJ support. As part of this migration, the (experimental v2) API path moved from `/api/cluster/package` to `/api/cluster/packages`, and requests for a non-existent package or package version now return HTTP 404 instead of 400. | ||
| type: changed | ||
| authors: | ||
| - name: Eric Pugh | ||
| links: | ||
| - name: PR#4178 | ||
| url: https://github.com/apache/solr/pull/4178 | ||
| - name: SOLR-18212 | ||
| url: https://issues.apache.org/jira/browse/SOLR-18212 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| /* | ||
| * 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.endpoint; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| 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.Path; | ||
| import jakarta.ws.rs.PathParam; | ||
| import jakarta.ws.rs.QueryParam; | ||
| import org.apache.solr.client.api.model.AddPackageVersionRequestBody; | ||
| import org.apache.solr.client.api.model.GetPackageResponse; | ||
| import org.apache.solr.client.api.model.ListPackagesResponse; | ||
| import org.apache.solr.client.api.model.SolrJerseyResponse; | ||
|
|
||
| /** V2 API definitions for managing Solr packages. */ | ||
| @Path("/cluster/packages") | ||
| public interface PackageApis { | ||
|
|
||
| @GET | ||
| @Operation( | ||
| summary = "List all packages registered in this Solr cluster.", | ||
| tags = {"package"}) | ||
| ListPackagesResponse listPackages( | ||
| @Parameter( | ||
| description = | ||
| "If provided, the node waits until its package data matches this ZooKeeper version.") | ||
| @QueryParam("expectedVersion") | ||
| Integer expectedVersion); | ||
|
|
||
| @GET | ||
| @Path("/{packageName}") | ||
| @Operation( | ||
| summary = "Get information about a specific package in this Solr cluster.", | ||
| tags = {"package"}) | ||
| GetPackageResponse getPackage( | ||
| @Parameter(description = "The name of the package.", required = true) | ||
| @PathParam("packageName") | ||
| String packageName, | ||
| @Parameter( | ||
| description = | ||
| "If provided, the node waits until its package data matches this ZooKeeper version before responding.") | ||
| @QueryParam("expectedVersion") | ||
| Integer expectedVersion); | ||
|
|
||
| @POST | ||
| @Path("/{packageName}/versions") | ||
| @Operation( | ||
| summary = "Add a version of a package to this Solr cluster.", | ||
| tags = {"package"}) | ||
| SolrJerseyResponse addPackageVersion( | ||
| @Parameter(description = "The name of the package.", required = true) | ||
| @PathParam("packageName") | ||
| String packageName, | ||
| @RequestBody(description = "Details of the package version to add.", required = true) | ||
| AddPackageVersionRequestBody requestBody); | ||
|
|
||
| @DELETE | ||
| @Path("/{packageName}/versions/{version}") | ||
| @Operation( | ||
| summary = "Delete a specific version of a package from this Solr cluster.", | ||
| tags = {"package"}) | ||
| SolrJerseyResponse deletePackageVersion( | ||
| @Parameter(description = "The name of the package.", required = true) | ||
| @PathParam("packageName") | ||
| String packageName, | ||
| @Parameter(description = "The version of the package to delete.", required = true) | ||
| @PathParam("version") | ||
| String version); | ||
|
|
||
| @POST | ||
| @Path("/{packageName}/refresh") | ||
| @Operation( | ||
| summary = "Refresh a package on all nodes in this Solr cluster.", | ||
| tags = {"package"}) | ||
| SolrJerseyResponse refreshPackage( | ||
| @Parameter(description = "The name of the package to refresh.", required = true) | ||
| @PathParam("packageName") | ||
| String packageName, | ||
| @Parameter( | ||
| description = | ||
| "Indicates whether the refresh should only be done on the receiving node. For internal use only.") | ||
| @QueryParam("localOnly") | ||
| Boolean localOnly); | ||
|
|
||
| @POST | ||
| @Path("/sync") | ||
| @Operation( | ||
| summary = | ||
| "Block until the receiving node's package data matches the given ZooKeeper version. " | ||
| + "Used internally for inter-node fan-out after a package change; not normally invoked directly.", | ||
| tags = {"package"}) | ||
| SolrJerseyResponse syncPackages( | ||
| @Parameter( | ||
| description = "The ZooKeeper version that the receiving node should catch up to.", | ||
| required = true) | ||
| @QueryParam("expectedVersion") | ||
| Integer expectedVersion); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /* | ||
| * 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; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import java.util.List; | ||
|
|
||
| /** Request body for adding a version of a package. */ | ||
| public class AddPackageVersionRequestBody { | ||
|
|
||
| @JsonProperty("version") | ||
| @Schema(description = "The version string for this package version.", required = true) | ||
|
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.
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. thanks, going to look....
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. Confirmed via the compiler (
|
||
| public String version; | ||
|
|
||
| @JsonProperty("files") | ||
| @Schema( | ||
| description = "File paths from the file store to include in this version.", | ||
| required = true) | ||
| public List<String> files; | ||
|
|
||
| @JsonProperty("manifest") | ||
| @Schema(description = "Optional path to a manifest file in the file store.") | ||
| public String manifest; | ||
|
|
||
| @JsonProperty("manifestSHA512") | ||
| @Schema(description = "Optional SHA-512 hash of the manifest file.") | ||
| public String manifestSHA512; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /* | ||
| * 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; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** Response for the package listing API. */ | ||
| public class ListPackagesResponse extends SolrJerseyResponse { | ||
|
|
||
| @JsonProperty("packages") | ||
| @Schema(description = "Map from package name to list of package versions.") | ||
| public Map<String, List<PackageVersion>> packages; | ||
|
|
||
| @JsonProperty("znodeVersion") | ||
| @Schema(description = "The ZooKeeper version of the packages.json node.") | ||
| public int znodeVersion; | ||
|
|
||
| /** Describes a single version of a package. */ | ||
| public static class PackageVersion { | ||
| @JsonProperty("package") | ||
| @Schema(description = "The package name.") | ||
| public String pkg; | ||
|
|
||
| @JsonProperty("version") | ||
| @Schema(description = "The version string.") | ||
| public String version; | ||
|
|
||
| @JsonProperty("files") | ||
| @Schema(description = "List of file paths from the file store included in this version.") | ||
| public List<String> files; | ||
|
|
||
| @JsonProperty("manifest") | ||
| @Schema(description = "Optional manifest reference.") | ||
| public String manifest; | ||
|
|
||
| @JsonProperty("manifestSHA512") | ||
| @Schema(description = "Optional SHA-512 hash of the manifest.") | ||
| public String manifestSHA512; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,12 +50,10 @@ | |
| import org.apache.solr.client.solrj.SolrRequest; | ||
| import org.apache.solr.client.solrj.SolrServerException; | ||
| import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; | ||
| import org.apache.solr.client.solrj.request.GenericSolrRequest; | ||
| import org.apache.solr.client.solrj.request.FileStoreApi; | ||
| import org.apache.solr.client.solrj.request.GenericV2SolrRequest; | ||
| import org.apache.solr.client.solrj.request.V2Request; | ||
| import org.apache.solr.client.solrj.request.beans.PackagePayload; | ||
| import org.apache.solr.client.solrj.request.PackageApi; | ||
| import org.apache.solr.client.solrj.request.beans.PluginMeta; | ||
| import org.apache.solr.client.solrj.response.V2Response; | ||
| import org.apache.solr.common.SolrException; | ||
| import org.apache.solr.common.SolrException.ErrorCode; | ||
| import org.apache.solr.common.cloud.SolrZkClient; | ||
|
|
@@ -150,20 +148,9 @@ public void uninstall(String packageName, String version) | |
| // Delete the package by calling the Package API and remove the Jar | ||
|
|
||
| runtime.printSuccess("Executing Package API to remove this package..."); | ||
| PackagePayload.DelVersion del = new PackagePayload.DelVersion(); | ||
| del.version = version; | ||
| del.pkg = packageName; | ||
|
|
||
| V2Request req = | ||
| new V2Request.Builder("/cluster/package") | ||
| .forceV2(true) | ||
| .withMethod(SolrRequest.METHOD.POST) | ||
| .withPayload(Map.of("delete", del)) | ||
| .build(); | ||
|
|
||
| try { | ||
| V2Response resp = req.process(solrClient); | ||
| runtime.printSuccess("Response: " + resp.jsonStr()); | ||
| new PackageApi.DeletePackageVersion(packageName, version).process(solrClient); | ||
|
epugh marked this conversation as resolved.
Resolved
|
||
| runtime.printSuccess("Package version deleted from Package API."); | ||
| } catch (SolrServerException | IOException e) { | ||
| throw new SolrException(ErrorCode.BAD_REQUEST, e); | ||
| } | ||
|
|
@@ -176,9 +163,8 @@ public void uninstall(String packageName, String version) | |
| String.format(Locale.ROOT, "/package/%s/%s/%s", packageName, version, "manifest.json")); | ||
| for (String filePath : filesToDelete) { | ||
| DistribFileStore.deleteZKFileEntry(zkClient, filePath); | ||
| String path = "/api/cluster/filestore/files" + filePath; | ||
| runtime.printSuccess("Deleting " + path); | ||
| solrClient.request(new GenericSolrRequest(SolrRequest.METHOD.DELETE, path)); | ||
| runtime.printSuccess("Deleting " + filePath); | ||
| new FileStoreApi.DeleteFile(filePath).process(solrClient); | ||
| } | ||
|
|
||
| runtime.printSuccess("Package uninstalled: " + packageName + ":" + version + ":-)"); | ||
|
|
@@ -461,11 +447,8 @@ private Pair<List<String>, List<String>> deployCollectionPackage( | |
| // If updating, refresh the package version for this to take effect | ||
| if (isUpdate || pegToLatest) { | ||
| try { | ||
| SolrCLI.postJsonToSolr( | ||
| solrClient, | ||
| PackageUtils.PACKAGE_PATH, | ||
| "{\"refresh\": \"" + packageInstance.name + "\"}"); | ||
| } catch (Exception ex) { | ||
| new PackageApi.RefreshPackage(packageInstance.name).process(solrClient); | ||
| } catch (SolrServerException | IOException ex) { | ||
| throw new SolrException(ErrorCode.SERVER_ERROR, ex); | ||
| } | ||
| } | ||
|
|
@@ -1074,14 +1057,12 @@ public void undeploy( | |
|
|
||
| // Set the package version in the collection's parameters | ||
| try { | ||
| // Is it better to "unset"? If so, build support in params API for "unset" | ||
| SolrCLI.postJsonToSolr( | ||
| solrClient, | ||
| PackageUtils.getCollectionParamsPath(collection), | ||
| "{set: {PKG_VERSIONS: {" + packageName + ": null}}}"); | ||
| SolrCLI.postJsonToSolr( | ||
| solrClient, PackageUtils.PACKAGE_PATH, "{\"refresh\": \"" + packageName + "\"}"); | ||
| } catch (Exception ex) { | ||
| new PackageApi.RefreshPackage(packageName).process(solrClient); | ||
| } catch (Exception ex) { // postJsonToSolr above declares a plain throws Exception | ||
|
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. this code comment still valid? |
||
| throw new SolrException(ErrorCode.SERVER_ERROR, ex); | ||
| } | ||
|
|
||
|
|
||

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.
Per
dev-docs/apis-v2-conventions.adoc's "Unproxied APIs" section: paths like/api/node/keyexist specifically to mark operations that only affect the receiving node and must never be proxied.syncPackages()fits that exactly -- it only ever touches the localPackageStore(seeClusterPackage.syncToVersion), and is invoked exclusively viabroadcastToLiveNodes()/processWithBaseUrl()targeting one specific node's base URL. The@Operationsummary here says as much: "Used internally for inter-node fan-out after a package change; not normally invoked directly."Recommend
POST /api/node/packages/syncinstead of/cluster/packages/sync, following the existing/node/{resource}pattern (NodeSystemInfoApi→/node/system,NodeHealthApi→/node/health, and especiallyGetNodeCommandStatusApi→/node/commands/, the closest precedent as another receiving-node-only, not-cluster-tracked concept) combined with the "command API" convention already used elsewhere in this PR (refreshPackage→.../refresh):packagesas the resource,syncas the appended command verb. Only this one endpoint would need to move --listPackages/getPackageare genuinely cluster-wide, proxy-friendly reads and should stay under/cluster/packages.