solr_cloud-connection 0.6.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da67e9a6312ae058259368a9933abe711d1c0554337c59c5f688cf2f17cee606
4
- data.tar.gz: 36b815217b2f3e806d9b639d4839f093d61327ee496fee1c3d4774039c688e48
3
+ metadata.gz: 19bcfd12d8bc2169aad5f3a01163996e671c0e8b2594ad50197ee7127abf15a9
4
+ data.tar.gz: 9088cfcdf21b11fe5ac92fd35ea2d5ec66ea55471d9a0e3a16edf2304db22bf4
5
5
  SHA512:
6
- metadata.gz: c828e6c7f7976cee46ac7b81c8955d2253253e3084e3841e26a502bf2d03808327578e69e63a47c0907dd7d38df3db27747a2383a6e8a6f9f4b1a73c69832340
7
- data.tar.gz: 25548a6825b308a62ec376bf1b59d596a557c8508f9feeb56cabd5bc402907ecf4f7602658c5087483424844d1fffddf2c7673ecc2f16cefb3ad79f03696aba5
6
+ metadata.gz: 61d0a5ce50ec30182bc0e60c915a78b6cc492039291634b53e48efb5fd91124643c97ebafc9ad9043d690bf0f28dbd047c5d1688acfb927c5ca126320d9e00c7
7
+ data.tar.gz: 9771f4d1047ef1210eecd1fc78b376363c7d9d7cd34b4c72023eb9f502aa746c1247844de176d03c9c3db8aec9673fa9d65edd97eb06c7f4d9bb18ec7a70a217
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0] - 2026-07-07
4
+
5
+ ### Major — Solr 8/9/10 Dual-Version Compatibility
6
+
7
+ This release adds support for Solr 9 and 10 alongside the existing Solr 8 support.
8
+ The internal API dispatch now detects the connected Solr major version and routes
9
+ requests to the correct endpoint, since several Solr APIs changed paths or response
10
+ envelopes between Solr 8 and Solr 9/10.
11
+
12
+ - **Feature: Solr 8/9/10 compatibility**
13
+ - Use V1 `CLUSTERSTATUS` action for collection info instead of V2 `api/collections/{name}`
14
+ (the V2 response envelope changed between Solr 8 and Solr 9/10 — the `cluster` wrapper
15
+ key is absent in Solr 10)
16
+ - Use V1 `LISTALIASES` action with `|| {}` guard for nil aliases on Solr 10
17
+ - Use V1 `admin/configs` endpoint for configset upload (path differs between Solr 8 and 9+)
18
+ - Add `solr9_or_later?` predicate to the connection for external version-gating
19
+ - **CI: Matrix testing** — run tests against Solr 8 & Solr 10 in parallel via GitHub Actions
20
+ - **Infrastructure** — Docker Compose healthchecks, env setup for Solr 10 test container, curl-based wait loops
21
+
22
+ ### Chores
23
+ - Replace `wait-for-it` / raw curl loops with Docker Compose healthcheck + `docker compose wait`
24
+ - Add Solr 10 Docker image definition (`spec/data/solr10_docker/`)
25
+ - Update README
26
+ - Version bump to 1.0.0
27
+
28
+ ## [0.6.0] - 2025-05-29
29
+ - Expose `username` and `password` getters on the connection
30
+ - Update README with "Common Usage" section
31
+ - Peg dependency versions in gemspec
32
+ - General gemfile/spec cleanup
33
+
3
34
  ## [0.5.0] - 2025-03-24
4
35
  - Added some sugar for unifying aliases and collections
5
36
  - Update tests
data/LICENSE.md ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2026, The Regents of the University of Michigan.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are
6
+ met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+ * Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+ * Neither the name of the The University of Michigan nor the
14
+ names of its contributors may be used to endorse or promote products
15
+ derived from this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE REGENTS OF THE UNIVERSITY OF MICHIGAN AND
18
+ CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19
+ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OF THE
21
+ UNIVERSITY OF MICHIGAN BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
23
+ TO,PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
27
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/compose.yml CHANGED
@@ -11,21 +11,56 @@ services:
11
11
  - .env
12
12
  - env.test
13
13
  command: "tail -f /dev/null"
14
+ depends_on:
15
+ solr:
16
+ condition: service_healthy
17
+ solr10:
18
+ condition: service_healthy
14
19
 
15
20
  solr:
16
21
  build: spec/data/solr_docker/.
17
22
  ports:
18
- - "9090:8983"
23
+ - "6781:8983"
19
24
  environment:
20
25
  - ZK_HOST=zoo:2181
21
26
  depends_on:
22
27
  - zoo
23
28
  command: solr-foreground
29
+ healthcheck:
30
+ test: ["CMD-SHELL", "curl -sf -u solr:SolrRocks http://localhost:8983/solr/admin/info/system || exit 1"]
31
+ interval: 10s
32
+ timeout: 5s
33
+ retries: 12
34
+ start_period: 30s
24
35
 
25
36
  zoo:
26
37
  image: zookeeper
27
38
  ports:
28
- - 9091:2181
39
+ - 6782:2181
40
+ environment:
41
+ ZOO_MY_ID: 1
42
+ ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181
43
+
44
+ solr10:
45
+ build: spec/data/solr10_docker/.
46
+ ports:
47
+ - "6783:8983"
48
+ environment:
49
+ - ZK_HOST=zoo10:2181
50
+ depends_on:
51
+ - zoo10
52
+ command: solr-foreground
53
+ healthcheck:
54
+ test: ["CMD-SHELL", "curl -sf -u solr:SolrRocks http://localhost:8983/solr/admin/info/system || exit 1"]
55
+ interval: 10s
56
+ timeout: 5s
57
+ retries: 12
58
+ start_period: 30s
59
+
60
+ zoo10:
61
+ image: zookeeper
62
+ ports:
63
+ - 6784:2181
29
64
  environment:
30
65
  ZOO_MY_ID: 1
31
66
  ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181
data/env.test.solr10 ADDED
@@ -0,0 +1,3 @@
1
+ SOLR_URL="http://solr10:8983"
2
+ SOLR_PASSWORD=SolrRocks
3
+ SOLR_USER=solr
@@ -98,8 +98,15 @@ module SolrCloud
98
98
 
99
99
  # Access to the root info from the api. Mostly for internal use, but not labeled as such
100
100
  # 'cause users will almost certainly find a use for it.
101
+ #
102
+ # Uses the V1 +CLUSTERSTATUS+ action rather than the V2 +api/collections/{name}+
103
+ # endpoint because the V2 response envelope changed between Solr 8 and Solr 9/10
104
+ # (the +cluster+ wrapper key is absent in Solr 10). The V1 CLUSTERSTATUS response
105
+ # consistently returns +cluster.collections.{name}+ on all supported versions.
106
+ #
107
+ # @return [Hash] the collection status hash as returned by Solr
101
108
  def info
102
- connection.get("api/collections/#{name}").body["cluster"]["collections"][name]
109
+ connection.get("solr/admin/collections", action: "CLUSTERSTATUS", collection: name).body["cluster"]["collections"][name]
103
110
  end
104
111
 
105
112
  # Reported as healthy?
@@ -74,10 +74,16 @@ module SolrCloud
74
74
  end
75
75
  end
76
76
 
77
- # The "raw" alias map, which just maps alias names to collection names
78
- # @return [Hash<String, String>]
77
+ # The "raw" alias map, which just maps alias names to collection names.
78
+ #
79
+ # The Solr +LISTALIASES+ action omits the +"aliases"+ key entirely (returning +null+
80
+ # or an absent key) when no aliases are defined. This is true on Solr 10 and can
81
+ # occur on earlier versions as well. The +|| {}+ guard ensures callers always receive
82
+ # a Hash rather than +nil+.
83
+ #
84
+ # @return [Hash<String, String>] map of alias name -> collection name, empty if none exist
79
85
  def raw_alias_map
80
- connection.get("solr/admin/collections", action: "LISTALIASES").body["aliases"]
86
+ connection.get("solr/admin/collections", action: "LISTALIASES").body["aliases"] || {}
81
87
  end
82
88
  end
83
89
  end
@@ -4,15 +4,30 @@ require "zip"
4
4
 
5
5
  module SolrCloud
6
6
  class Connection
7
- # methods having to do with configsets, to be included by the connection object.
7
+ # Methods having to do with configsets, to be included by the connection object.
8
8
  # These are split out only to make it easier to deal with them.
9
9
  module ConfigsetAdmin
10
10
  # Given the path to a solr configuration "conf" directory (i.e., the one with
11
11
  # solrconfig.xml in it), zip it up and send it to solr as a new configset.
12
+ #
13
+ # The upload API endpoint differs across major Solr versions:
14
+ #
15
+ # - *Solr 8*: uses the V2 API (+PUT /api/cluster/configs/<name>+), which was the
16
+ # canonical configset path in Solr 8.
17
+ # - *Solr 9+*: uses the V2 API (+PUT /api/configsets/<name>+).
18
+ # The V2 configset path moved from +/api/cluster/configs/<name>+ (Solr 8) to
19
+ # +/api/configsets/<name>+ (Solr 9+). The new V2 PUT defaults to overwriting an
20
+ # existing configset, which means in-use configsets can be replaced without
21
+ # deleting them first.
22
+ #
23
+ # Dispatches via {Connection#solr9_or_later?}; see {#upload_configset_v9} and
24
+ # {#upload_configset_v8} for the version-specific implementations.
25
+ #
12
26
  # @param name [String] Name to give the new configset
13
27
  # @param confdir [String, Pathname] Path to the solr configuration "conf" directory
14
28
  # @param force [Boolean] Whether or not to overwrite an existing configset if there is one
15
- # @raise [WontOverwriteError] if the configset already exists and "force" is false
29
+ # @raise [IllegalNameError] if +name+ is not a valid Solr identifier
30
+ # @raise [WontOverwriteError] if the configset already exists and +force+ is false
16
31
  # @return [Configset] the configset created
17
32
  def create_configset(name:, confdir:, force: false)
18
33
  config_set_name = name
@@ -23,36 +38,58 @@ module SolrCloud
23
38
  if has_configset?(config_set_name) && !force
24
39
  raise WontOverwriteError.new("Won't replace configset #{config_set_name} unless 'force: true' passed ")
25
40
  end
41
+
26
42
  zfile = "#{Dir.tmpdir}/solr_add_configset_#{name}_#{Time.now.hash}.zip"
27
- z = ZipFileGenerator.new(confdir, zfile)
28
- z.write
29
- @connection.put("api/cluster/configs/#{config_set_name}") do |req|
30
- req.body = File.binread(zfile)
43
+ ZipFileGenerator.new(confdir, zfile).write
44
+
45
+ if solr9_or_later?
46
+ upload_configset_v9(config_set_name, zfile)
47
+ else
48
+ upload_configset_v8(config_set_name, zfile)
31
49
  end
32
- # TODO: Error check in here somewhere
33
- FileUtils.rm(zfile, force: true)
50
+
34
51
  get_configset(name)
52
+ ensure
53
+ FileUtils.rm(zfile, force: true) if zfile
35
54
  end
36
55
 
37
- # Get a list of the already-defined configSets
38
- # @return [Array<Configset>] possibly empty list of configSets
56
+ # Get a list of the already-defined configsets.
57
+ # @return [Array<Configset>] possibly empty list of configsets
39
58
  def configsets
40
59
  configset_names.map { |cs| Configset.new(name: cs, connection: self) }
41
60
  end
42
61
 
62
+ # Get the names of all defined configsets.
63
+ #
64
+ # The API endpoint differs across major Solr versions:
65
+ #
66
+ # - *Solr 8*: uses the V2 API (+GET /api/cluster/configs+).
67
+ # - *Solr 9+*: uses the V1 API (+GET /solr/admin/configs?action=LIST+).
68
+ # The V2 configset path changed between Solr 8 and Solr 9; V1 is used for
69
+ # consistency on Solr 9 and later.
70
+ #
43
71
  # @return [Array<String>] the names of the config sets
44
72
  def configset_names
45
- connection.get("api/cluster/configs").body["configSets"]
73
+ if solr9_or_later?
74
+ connection.get("solr/admin/configs", action: "LIST").body["configSets"]
75
+ else
76
+ connection.get("api/cluster/configs").body["configSets"]
77
+ end
46
78
  end
47
79
 
48
- # Check to see if a configset is defined
49
- # @param name [String] Name of the configSet
80
+ # Check to see if a configset with the given name is defined.
81
+ # @param name [String] Name of the configset
50
82
  # @return [Boolean] Whether a configset with that name exists
51
83
  def has_configset?(name)
52
84
  configset_names.include? name.to_s
53
85
  end
54
86
 
55
- # Get an existing configset
87
+ # Get an existing configset by name.
88
+ #
89
+ # @note Does not verify that the configset actually exists; use {#has_configset?}
90
+ # to check first if needed.
91
+ # @param name [String] Name of the configset
92
+ # @return [Configset]
56
93
  def get_configset(name)
57
94
  Configset.new(name: name, connection: self)
58
95
  end
@@ -63,12 +100,25 @@ module SolrCloud
63
100
  #
64
101
  # In general, prefer using {Configset#delete!} instead of running everything
65
102
  # through the connection object.
66
- # @param [String] name The name of the configuration set
67
- # @raise [InUseError] if the configset can't be deleted because it's in use by a live collection
103
+ #
104
+ # The delete API endpoint differs across major Solr versions:
105
+ #
106
+ # - *Solr 8*: uses the V2 API (+DELETE /api/cluster/configs/<name>+).
107
+ # - *Solr 9+*: uses the V1 API (+GET /solr/admin/configs?action=DELETE+).
108
+ # The V2 configset path changed between Solr 8 and Solr 9; V1 is used for
109
+ # consistency on Solr 9 and later.
110
+ #
111
+ # @param name [String] The name of the configuration set
112
+ # @raise [ConfigSetInUseError] if the configset can't be deleted because it's in use
113
+ # by a live collection
68
114
  # @return [Connection] self
69
115
  def delete_configset(name)
70
- if has_configset? name
71
- connection.delete("api/cluster/configs/#{name}")
116
+ if has_configset?(name)
117
+ if solr9_or_later?
118
+ connection.get("solr/admin/configs", action: "DELETE", name: name)
119
+ else
120
+ connection.delete("api/cluster/configs/#{name}")
121
+ end
72
122
  end
73
123
  self
74
124
  rescue Faraday::BadRequestError => e
@@ -125,6 +175,43 @@ module SolrCloud
125
175
  zipfile.add(zipfile_path, disk_file_path)
126
176
  end
127
177
  end
178
+
179
+ private
180
+
181
+ # Upload a configset zip using the Solr 9+ V2 API.
182
+ #
183
+ # Sends a +PUT+ to +/api/configsets/<name>+ with the zip file as an
184
+ # octet-stream body. The V2 configset upload path moved from
185
+ # +/api/cluster/configs/<name>+ (Solr 8) to +/api/configsets/<name>+ (Solr 9+).
186
+ # The new V2 PUT defaults to overwriting any existing configset at that name,
187
+ # so in-use configsets can be replaced without deleting first.
188
+ #
189
+ # @param name [String] configset name
190
+ # @param zipfile_path [String] filesystem path to the zip archive to upload
191
+ # @return [void]
192
+ def upload_configset_v9(name, zipfile_path)
193
+ connection.put("api/configsets/#{name}") do |req|
194
+ req.headers["Content-Type"] = "application/octet-stream"
195
+ req.body = File.binread(zipfile_path)
196
+ end
197
+ end
198
+
199
+ # Upload a configset zip using the Solr 8 V2 API.
200
+ #
201
+ # Sends a +PUT+ to +/api/cluster/configs/<name>+ with the zip file as an
202
+ # octet-stream body. This was the canonical V2 configset upload path in Solr 8.
203
+ # The path changed to +/api/configsets/<name>+ in Solr 9; use {#upload_configset_v1}
204
+ # for Solr 9 and later.
205
+ #
206
+ # @param name [String] configset name
207
+ # @param zipfile_path [String] filesystem path to the zip archive to upload
208
+ # @return [void]
209
+ def upload_configset_v8(name, zipfile_path)
210
+ connection.put("api/cluster/configs/#{name}") do |req|
211
+ req.headers["Content-Type"] = "application/octet-stream"
212
+ req.body = File.binread(zipfile_path)
213
+ end
214
+ end
128
215
  end
129
216
  end
130
217
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SolrCloud
4
4
  class Connection
5
- VERSION = "0.6.0"
5
+ VERSION = "1.0.0"
6
6
  end
7
7
  end
@@ -176,6 +176,17 @@ module SolrCloud
176
176
  _version_part_int(2)
177
177
  end
178
178
 
179
+ # Returns true if the connected Solr instance is version 9 or later (9, 10, 11, ...).
180
+ #
181
+ # Several Solr APIs changed paths between Solr 8 and Solr 9. This predicate is used
182
+ # internally to dispatch to the correct API endpoint when behavior differs across major
183
+ # versions. External callers may also use it to gate version-specific logic.
184
+ #
185
+ # @return [Boolean] true if the server reports major version >= 9
186
+ def solr9_or_later?
187
+ major_version >= 9
188
+ end
189
+
179
190
  # Check to see if the given string follows solr's rules for thing
180
191
  # Solr only allows ASCII letters and numbers, underscore, and dash,
181
192
  # and it can't start with an underscore.
@@ -0,0 +1,290 @@
1
+ # Solr 8 / 9 / 10 Compatibility Update Plan
2
+
3
+ ## Overview
4
+
5
+ Make `solr_cloud-connection` work correctly with Solr 8, 9, and 10 simultaneously. The approach:
6
+ branch on `major_version` where APIs differ, and use V1 (legacy) APIs where they work uniformly
7
+ across versions.
8
+
9
+ Version detection is already in place in `connection.rb` via `major_version`, `minor_version`,
10
+ and `patch_version` methods derived from `GET /solr/admin/info/system`.
11
+
12
+ Source examined: `main` branch and `fix/solr-10-api-compatibility` branch.
13
+ API docs source: Apache Solr Reference Guide 10.0 (live, fetched May 2026).
14
+
15
+ ---
16
+
17
+ ## Root Cause
18
+
19
+ The main branch uses Solr 8-era V2 API paths for configset management
20
+ (`api/cluster/configs/*`). These paths changed in Solr 9/10 to `api/configsets/*`.
21
+ Additionally, the V1 `LISTALIASES` action returns `null` (not `{}`) in Solr 10 when
22
+ no aliases exist, causing a `NoMethodError` on `nil.keys`.
23
+
24
+ The `fix/solr-10-api-compatibility` branch already addresses this by switching configset
25
+ operations wholesale to V1 APIs and adding a `|| {}` nil guard, but it does so without
26
+ version branching. This plan implements proper version-based dispatch.
27
+
28
+ ---
29
+
30
+ ## API Compatibility Matrix
31
+
32
+ | Method / File | Solr 8 (main branch) | Solr 9/10 compat? | Notes |
33
+ |---|---|---|---|
34
+ | `configset_admin#create_configset` | `PUT api/cluster/configs/{name}` (V2) | NO | V2 path changed |
35
+ | `configset_admin#configset_names` | `GET api/cluster/configs` (V2) | NO | V2 path changed |
36
+ | `configset_admin#delete_configset` | `DELETE api/cluster/configs/{name}` (V2) | NO | V2 path changed |
37
+ | `alias_admin#raw_alias_map` | `GET solr/admin/collections?action=LISTALIASES` | PARTIAL | returns nil when empty |
38
+ | `alias_admin#create_alias` | V1 CREATEALIAS | YES | V1 works in all versions |
39
+ | `alias#delete!` | V1 DELETEALIAS | YES | V1 works in all versions |
40
+ | `collection_admin#only_collections` | `GET api/collections` (V2) | YES | same response format |
41
+ | `collection_admin#create_collection` | V1 CREATE | YES | V1 works in all versions |
42
+ | `collection#delete!` | V1 DELETE | YES | V1 works in all versions |
43
+ | `collection#info` | `GET api/collections/{name}` (V2) | UNKNOWN | needs testing; see below |
44
+
45
+ ---
46
+
47
+ ## Changes Required
48
+
49
+ ### 1. `connection.rb` -- add version-helper predicate
50
+
51
+ Add a convenience predicate to avoid repeating `major_version >= 9` comparisons inline:
52
+
53
+ ```ruby
54
+ # Returns true for Solr 9 and later (including 10, 11, ...)
55
+ def solr9_or_later?
56
+ major_version >= 9
57
+ end
58
+ ```
59
+
60
+ Place it alongside the existing `major_version` / `minor_version` / `patch_version` methods.
61
+ No changes to `bail_if_incompatible!`.
62
+
63
+ ---
64
+
65
+ ### 2. `connection/configset_admin.rb` -- version-branch all three methods
66
+
67
+ The V2 configset paths changed between Solr 8 and Solr 9:
68
+
69
+ | Operation | Solr 8 V2 path | Solr 9/10 V2 path | V1 path (all versions) |
70
+ |---|---|---|---|
71
+ | Upload | `PUT api/cluster/configs/{name}` | `PUT api/configsets/{name}` | `POST solr/admin/configs?action=UPLOAD&name={name}` |
72
+ | List | `GET api/cluster/configs` -> `["configSets"]` | `GET api/configsets` -> `["configSets"]` | `GET solr/admin/configs?action=LIST` -> `["configSets"]` |
73
+ | Delete | `DELETE api/cluster/configs/{name}` | `DELETE api/configsets/{name}` | `GET solr/admin/configs?action=DELETE&name={name}` |
74
+
75
+ Strategy: use V1 for Solr 9+, keep existing V2 for Solr 8. V1 works in Solr 8 too, so
76
+ an alternative is to use V1 for all versions and avoid branching entirely -- but the task
77
+ requires explicit version dispatch.
78
+
79
+ #### `create_configset`
80
+
81
+ ```ruby
82
+ def create_configset(name:, confdir:, force: false)
83
+ raise WontOverwriteError.new("Configset '#{name}' already exists") if has_configset?(name) && !force
84
+ zipfile = zip_configset_dir(confdir)
85
+ if connection.solr9_or_later?
86
+ connection.post("solr/admin/configs?action=UPLOAD&name=#{name}") do |req|
87
+ req.headers["Content-Type"] = "application/octet-stream"
88
+ req.body = File.binread(zipfile)
89
+ end
90
+ else
91
+ connection.put("api/cluster/configs/#{name}") do |req|
92
+ req.headers["Content-Type"] = "application/octet-stream"
93
+ req.body = File.binread(zipfile)
94
+ end
95
+ end
96
+ get_configset(name)
97
+ ensure
98
+ FileUtils.rm_f(zipfile) if zipfile
99
+ end
100
+ ```
101
+
102
+ Note: the `fix` branch uses `connection.post("solr/admin/configs", action: "UPLOAD",
103
+ name: name)` but that places action/name as query params on a POST. The Solr V1 API
104
+ docs specify these as query string parameters for the UPLOAD action, so either form works.
105
+ Verify with integration tests.
106
+
107
+ #### `configset_names`
108
+
109
+ ```ruby
110
+ def configset_names
111
+ if connection.solr9_or_later?
112
+ connection.get("solr/admin/configs", action: "LIST").body["configSets"]
113
+ else
114
+ connection.get("api/cluster/configs").body["configSets"]
115
+ end
116
+ end
117
+ ```
118
+
119
+ #### `delete_configset`
120
+
121
+ ```ruby
122
+ def delete_configset(name:)
123
+ return unless has_configset?(name)
124
+ if connection.solr9_or_later?
125
+ connection.get("solr/admin/configs", action: "DELETE", name: name)
126
+ else
127
+ connection.delete("api/cluster/configs/#{name}")
128
+ end
129
+ end
130
+ ```
131
+
132
+ ---
133
+
134
+ ### 3. `connection/alias_admin.rb` -- nil guard (bug fix, not version-specific)
135
+
136
+ `raw_alias_map` crashes with `NoMethodError` when no aliases exist because the Solr
137
+ `LISTALIASES` response omits the `aliases` key entirely (returns `null` or simply absent).
138
+ This is confirmed on Solr 10 and the fix is straightforward:
139
+
140
+ ```ruby
141
+ def raw_alias_map
142
+ connection.get("solr/admin/collections", action: "LISTALIASES").body["aliases"] || {}
143
+ end
144
+ ```
145
+
146
+ This fix is already present in `fix/solr-10-api-compatibility`. It is not version-specific --
147
+ apply it unconditionally, as it is defensive coding.
148
+
149
+ ---
150
+
151
+ ### 4. `lib/solr_cloud/collection.rb` -- verify `info` endpoint (needs testing)
152
+
153
+ `Collection#info` calls:
154
+
155
+ ```ruby
156
+ connection.get("api/collections/#{name}").body["cluster"]["collections"][name]
157
+ ```
158
+
159
+ The response path `body["cluster"]["collections"][name]` matches the structure returned by
160
+ the V1 `CLUSTERSTATUS` API and the V2 `GET /api/cluster` endpoint. Whether
161
+ `GET /api/collections/{name}` returns this same nested structure in Solr 9/10 is not
162
+ confirmed from docs alone.
163
+
164
+ **Action required:**
165
+ - Run integration tests against Solr 9 and Solr 10 containers with the existing code
166
+ - If `api/collections/{name}` returns a different response structure in Solr 9/10,
167
+ add version branching:
168
+
169
+ ```ruby
170
+ def info
171
+ if connection.solr9_or_later?
172
+ # Fallback to V1 CLUSTERSTATUS filtered to this collection
173
+ connection.get("solr/admin/collections",
174
+ action: "CLUSTERSTATUS",
175
+ collection: name
176
+ ).body["cluster"]["collections"][name]
177
+ else
178
+ connection.get("api/collections/#{name}").body["cluster"]["collections"][name]
179
+ end
180
+ end
181
+ ```
182
+
183
+ V1 `CLUSTERSTATUS` with `collection` param is confirmed to work in both Solr 8 and 10
184
+ and returns the exact same `cluster.collections.{name}` structure.
185
+
186
+ ---
187
+
188
+ ### 5. No changes needed
189
+
190
+ The following use V1 APIs or V2 APIs that are stable across Solr 8, 9, and 10:
191
+
192
+ - `alias_admin#create_alias` -- V1 `CREATEALIAS`
193
+ - `alias#delete!` -- V1 `DELETEALIAS`
194
+ - `collection_admin#create_collection` -- V1 `CREATE`
195
+ - `collection_admin#only_collections` -- `GET api/collections` response format confirmed
196
+ unchanged in Solr 10 (`{"collections": [...]}`)
197
+ - `collection#delete!` -- V1 `DELETE`
198
+ - `connection#system` / `version_string` / `major_version` etc. -- `/solr/admin/info/system`
199
+ unchanged
200
+
201
+ ---
202
+
203
+ ## Implementation Pattern
204
+
205
+ Use private dispatch methods to keep the public interface clean:
206
+
207
+ ```ruby
208
+ # In ConfigsetAdmin module:
209
+
210
+ def create_configset(name:, confdir:, force: false)
211
+ raise WontOverwriteError.new(...) if has_configset?(name) && !force
212
+ zipfile = zip_configset_dir(confdir)
213
+ solr9_or_later? ? upload_configset_v1(name, zipfile) : upload_configset_v8(name, zipfile)
214
+ get_configset(name)
215
+ ensure
216
+ FileUtils.rm_f(zipfile) if zipfile
217
+ end
218
+
219
+ private
220
+
221
+ def upload_configset_v1(name, zipfile)
222
+ connection.post("solr/admin/configs?action=UPLOAD&name=#{name}") do |req|
223
+ req.headers["Content-Type"] = "application/octet-stream"
224
+ req.body = File.binread(zipfile)
225
+ end
226
+ end
227
+
228
+ def upload_configset_v8(name, zipfile)
229
+ connection.put("api/cluster/configs/#{name}") do |req|
230
+ req.headers["Content-Type"] = "application/octet-stream"
231
+ req.body = File.binread(zipfile)
232
+ end
233
+ end
234
+ ```
235
+
236
+ Where the dispatch is simple (one line each branch), inline `if/else` is acceptable.
237
+ Prefer private methods when the per-version logic is more than 2 lines.
238
+
239
+ ---
240
+
241
+ ## Test Infrastructure
242
+
243
+ The existing spec infrastructure uses Docker Compose in `spec/data/solr_docker/`.
244
+
245
+ Add a Solr 10 service (and optionally Solr 9) alongside the existing Solr 8 service:
246
+
247
+ ```yaml
248
+ # In spec/data/solr_docker/docker-compose.yml (new service):
249
+ solr10:
250
+ image: solr:10-slim
251
+ ports:
252
+ - "18984:8983"
253
+ command: solr-demo
254
+ # or whatever start command the existing solr8 service uses
255
+ ```
256
+
257
+ Update the integration test runner (likely a Rake task or script) to run the full spec
258
+ suite twice: once against Solr 8 and once against Solr 10 (passing the base URL as an
259
+ environment variable).
260
+
261
+ If the test suite already accepts a `SOLR_URL` environment variable, this is straightforward.
262
+ If it hardcodes a port, parameterize it.
263
+
264
+ ---
265
+
266
+ ## Implementation Order
267
+
268
+ 1. Add `solr9_or_later?` to `connection.rb`
269
+ 2. Apply `|| {}` nil guard to `alias_admin.rb#raw_alias_map` (already in fix branch --
270
+ cherry-pick or re-apply)
271
+ 3. Version-branch `configset_admin.rb`: `configset_names`, `delete_configset`,
272
+ `create_configset` (in that order -- names and delete first so tests can verify the
273
+ cycle without broken upload)
274
+ 4. Add Solr 10 container to Docker Compose test setup
275
+ 5. Run integration tests against both Solr 8 and Solr 10
276
+ 6. If `Collection#info` fails against Solr 10, add version-branch (step 4 above)
277
+ 7. Update gem version (patch or minor, depending on whether the nil-guard fix is considered
278
+ a breaking change)
279
+
280
+ ---
281
+
282
+ ## Notes on the fix/solr-10-api-compatibility Branch
283
+
284
+ That branch's approach (switch all configset ops to V1 unconditionally) is functionally
285
+ correct and simpler. If backward compatibility with the V2 Solr 8 behavior is not a concern,
286
+ that approach is acceptable and lower-risk than version branching. The user has explicitly
287
+ requested version-based dispatch, which this plan implements.
288
+
289
+ If Solr 8 V2 API support is dropped in a future major version of the gem, the branches
290
+ can be collapsed to use V1 everywhere.
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solr_cloud-connection
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bill Dueber
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-09-18 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: faraday
@@ -154,7 +153,6 @@ dependencies:
154
153
  - - "~>"
155
154
  - !ruby/object:Gem::Version
156
155
  version: '3.0'
157
- description:
158
156
  email:
159
157
  - bill@dueber.com
160
158
  executables: []
@@ -166,11 +164,13 @@ files:
166
164
  - ".standard.yml"
167
165
  - CHANGELOG.md
168
166
  - Dockerfile
167
+ - LICENSE.md
169
168
  - LICENSE.txt
170
169
  - README.md
171
170
  - Rakefile
172
171
  - compose.yml
173
172
  - env.test
173
+ - env.test.solr10
174
174
  - lib/solr_cloud/alias.rb
175
175
  - lib/solr_cloud/collection.rb
176
176
  - lib/solr_cloud/configset.rb
@@ -181,13 +181,13 @@ files:
181
181
  - lib/solr_cloud/connection/version.rb
182
182
  - lib/solr_cloud/errors.rb
183
183
  - readme.rb
184
+ - solr10_compat_update_plan.md
184
185
  homepage: https://github.com/mlibrary/solr_cloud-connection
185
186
  licenses: []
186
187
  metadata:
187
188
  homepage_uri: https://github.com/mlibrary/solr_cloud-connection
188
189
  source_code_uri: https://github.com/mlibrary/solr_cloud-connection
189
190
  changelog_uri: https://github.com/mlibrary/solr_cloud-connection/CHANGELOG.md
190
- post_install_message:
191
191
  rdoc_options: []
192
192
  require_paths:
193
193
  - lib
@@ -202,8 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
202
  - !ruby/object:Gem::Version
203
203
  version: '0'
204
204
  requirements: []
205
- rubygems_version: 3.5.11
206
- signing_key:
205
+ rubygems_version: 3.6.7
207
206
  specification_version: 4
208
207
  summary: Do basic administrative operations on a solr cloud instance and collections
209
208
  within