From 73c243c7bbc211187a34554d75d4f14e81d42043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sun, 5 Apr 2026 04:00:56 +0200 Subject: [PATCH 01/19] Add picocli to project --- gradle/libs.versions.toml | 2 ++ solr/api/gradle.lockfile | 1 + solr/benchmark/gradle.lockfile | 1 + solr/core/build.gradle | 2 ++ solr/core/gradle.lockfile | 3 ++- solr/cross-dc-manager/gradle.lockfile | 1 + solr/modules/analysis-extras/gradle.lockfile | 1 + solr/modules/clustering/gradle.lockfile | 1 + solr/modules/cross-dc/gradle.lockfile | 1 + solr/modules/cuvs/gradle.lockfile | 1 + solr/modules/extraction/gradle.lockfile | 1 + solr/modules/gcs-repository/gradle.lockfile | 1 + solr/modules/jwt-auth/gradle.lockfile | 1 + solr/modules/langid/gradle.lockfile | 1 + solr/modules/language-models/gradle.lockfile | 1 + solr/modules/ltr/gradle.lockfile | 1 + solr/modules/opentelemetry/gradle.lockfile | 1 + solr/modules/s3-repository/gradle.lockfile | 1 + solr/modules/scripting/gradle.lockfile | 1 + solr/modules/sql/gradle.lockfile | 1 + solr/server/gradle.lockfile | 1 + solr/solr-ref-guide/gradle.lockfile | 1 + solr/solrj-jetty/gradle.lockfile | 1 + solr/solrj-streaming/gradle.lockfile | 1 + solr/solrj-zookeeper/gradle.lockfile | 1 + solr/solrj/gradle.lockfile | 1 + solr/test-framework/gradle.lockfile | 1 + solr/webapp/gradle.lockfile | 1 + 28 files changed, 31 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0bcff24a4457..a9222bc94c01 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -186,6 +186,7 @@ ow2-asm = "9.8" owasp-dependencycheck = "12.1.3" # @keep for version alignment perfmark = "0.27.0" +picocli = "4.7.6" prometheus-metrics = "1.1.0" prometheus-simpleclient = "0.16.0" quicktheories = "0.26" @@ -500,6 +501,7 @@ ow2-asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "ow2-asm" ow2-asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "ow2-asm" } # @keep transitive dependency for version alignment perfmark-api = { module = "io.perfmark:perfmark-api", version.ref = "perfmark" } +picocli = { module = "info.picocli:picocli", version.ref = "picocli" } prometheus-metrics-expositionformats = { module = "io.prometheus:prometheus-metrics-exposition-formats", version.ref = "prometheus-metrics" } prometheus-metrics-model = { module = "io.prometheus:prometheus-metrics-model", version.ref = "prometheus-metrics" } prometheus-simpleclient = { module = "io.prometheus:simpleclient", version.ref = "prometheus-simpleclient" } diff --git a/solr/api/gradle.lockfile b/solr/api/gradle.lockfile index e244a4e4778f..5a35b2e643df 100644 --- a/solr/api/gradle.lockfile +++ b/solr/api/gradle.lockfile @@ -37,6 +37,7 @@ com.tdunning:t-digest:3.3=jarValidation,testRuntimeClasspath commons-cli:commons-cli:1.10.0=jarValidation,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,testRuntimeClasspath commons-io:commons-io:2.20.0=jarValidation,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/benchmark/gradle.lockfile b/solr/benchmark/gradle.lockfile index 950df2762f00..dcdf6be5b49f 100644 --- a/solr/benchmark/gradle.lockfile +++ b/solr/benchmark/gradle.lockfile @@ -33,6 +33,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,testRuntimeClasspath commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,runtimeClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,runtimeClasspath,testRuntimeClasspath diff --git a/solr/core/build.gradle b/solr/core/build.gradle index f1bb21be4aa0..144a7445f9c7 100644 --- a/solr/core/build.gradle +++ b/solr/core/build.gradle @@ -98,6 +98,8 @@ dependencies { implementation libs.commonscodec.commonscodec implementation libs.commonscli.commonscli + implementation libs.picocli + permitUnusedDeclared libs.picocli // will be used when CLI is migrated to picocli implementation libs.locationtech.spatial4j diff --git a/solr/core/gradle.lockfile b/solr/core/gradle.lockfile index c1aa2fd5e140..ec0442ee4662 100644 --- a/solr/core/gradle.lockfile +++ b/solr/core/gradle.lockfile @@ -37,6 +37,7 @@ com.tdunning:t-digest:3.3=compileClasspath,jarValidation,runtimeClasspath,runtim commons-cli:commons-cli:1.10.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath commons-codec:commons-codec:1.19.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath @@ -187,4 +188,4 @@ org.slf4j:jcl-over-slf4j:2.0.17=apiHelper,jarValidation,runtimeClasspath,runtime org.slf4j:slf4j-api:2.0.17=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath org.xerial.snappy:snappy-java:1.1.10.8=jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath ua.net.nlp:morfologik-ukrainian-search:4.9.1=jarValidation,testRuntimeClasspath -empty=apiHelperTest,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestUnusedDeclared,permitTestUsedUndeclared,permitUnusedDeclared,permitUsedUndeclared,signatures +empty=apiHelperTest,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestUnusedDeclared,permitTestUsedUndeclared,permitUsedUndeclared,signatures diff --git a/solr/cross-dc-manager/gradle.lockfile b/solr/cross-dc-manager/gradle.lockfile index b3fb845f81a6..8366a00d8972 100644 --- a/solr/cross-dc-manager/gradle.lockfile +++ b/solr/cross-dc-manager/gradle.lockfile @@ -52,6 +52,7 @@ commons-collections:commons-collections:3.2.2=jarValidation,runtimeClasspath,run commons-digester:commons-digester:2.1=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath commons-validator:commons-validator:1.7=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/analysis-extras/gradle.lockfile b/solr/modules/analysis-extras/gradle.lockfile index 9094e0c4c838..f16a120d6f00 100644 --- a/solr/modules/analysis-extras/gradle.lockfile +++ b/solr/modules/analysis-extras/gradle.lockfile @@ -35,6 +35,7 @@ com.tdunning:t-digest:3.3=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,s commons-cli:commons-cli:1.10.0=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-io:commons-io:2.20.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/clustering/gradle.lockfile b/solr/modules/clustering/gradle.lockfile index 77e69877649f..0c4468e57f88 100644 --- a/solr/modules/clustering/gradle.lockfile +++ b/solr/modules/clustering/gradle.lockfile @@ -33,6 +33,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,runtimeLibs,solrPlatfor commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/cross-dc/gradle.lockfile b/solr/modules/cross-dc/gradle.lockfile index 8de3834cae8a..4cceb36b3540 100644 --- a/solr/modules/cross-dc/gradle.lockfile +++ b/solr/modules/cross-dc/gradle.lockfile @@ -34,6 +34,7 @@ com.tdunning:t-digest:3.3=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,s commons-cli:commons-cli:1.10.0=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-io:commons-io:2.20.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/cuvs/gradle.lockfile b/solr/modules/cuvs/gradle.lockfile index 0900760dfbc9..3ddcf49613c1 100644 --- a/solr/modules/cuvs/gradle.lockfile +++ b/solr/modules/cuvs/gradle.lockfile @@ -35,6 +35,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,runtimeLibs,solrPlatfor commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/extraction/gradle.lockfile b/solr/modules/extraction/gradle.lockfile index 5c56a4a84f45..d6654292ad61 100644 --- a/solr/modules/extraction/gradle.lockfile +++ b/solr/modules/extraction/gradle.lockfile @@ -36,6 +36,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,runtimeLibs,solrPlatfor commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/gcs-repository/gradle.lockfile b/solr/modules/gcs-repository/gradle.lockfile index 5afe73626a3a..f9b4f8067bad 100644 --- a/solr/modules/gcs-repository/gradle.lockfile +++ b/solr/modules/gcs-repository/gradle.lockfile @@ -62,6 +62,7 @@ com.tdunning:t-digest:3.3=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,s commons-cli:commons-cli:1.10.0=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/jwt-auth/gradle.lockfile b/solr/modules/jwt-auth/gradle.lockfile index 519c53f94d5d..010d84b4250a 100644 --- a/solr/modules/jwt-auth/gradle.lockfile +++ b/solr/modules/jwt-auth/gradle.lockfile @@ -43,6 +43,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,runtimeLibs,solrPlatfor commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/langid/gradle.lockfile b/solr/modules/langid/gradle.lockfile index 55f8c65b26cb..7958267f6cc6 100644 --- a/solr/modules/langid/gradle.lockfile +++ b/solr/modules/langid/gradle.lockfile @@ -34,6 +34,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,runtimeLibs,solrPlatfor commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/language-models/gradle.lockfile b/solr/modules/language-models/gradle.lockfile index f9973282836f..ddb7e258d6cd 100644 --- a/solr/modules/language-models/gradle.lockfile +++ b/solr/modules/language-models/gradle.lockfile @@ -48,6 +48,7 @@ dev.langchain4j:langchain4j-http-client:1.9.1=jarValidation,runtimeClasspath,run dev.langchain4j:langchain4j-hugging-face:1.9.1-beta17=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath dev.langchain4j:langchain4j-mistral-ai:1.9.1=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath dev.langchain4j:langchain4j-open-ai:1.9.1=jarValidation,runtimeClasspath,runtimeLibs,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/ltr/gradle.lockfile b/solr/modules/ltr/gradle.lockfile index 09c098e9d3fd..82d140e0e6a2 100644 --- a/solr/modules/ltr/gradle.lockfile +++ b/solr/modules/ltr/gradle.lockfile @@ -33,6 +33,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,runtimeLibs,solrPlatfor commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/opentelemetry/gradle.lockfile b/solr/modules/opentelemetry/gradle.lockfile index d952bc162f71..5bfb8f60825c 100644 --- a/solr/modules/opentelemetry/gradle.lockfile +++ b/solr/modules/opentelemetry/gradle.lockfile @@ -40,6 +40,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,runtimeLibs,solrPlatfor commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/s3-repository/gradle.lockfile b/solr/modules/s3-repository/gradle.lockfile index edca36eddb90..e11259d55458 100644 --- a/solr/modules/s3-repository/gradle.lockfile +++ b/solr/modules/s3-repository/gradle.lockfile @@ -46,6 +46,7 @@ com.tdunning:t-digest:3.3=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,s commons-cli:commons-cli:1.10.0=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=apiHelper,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/scripting/gradle.lockfile b/solr/modules/scripting/gradle.lockfile index fae41d9e6456..c0495633dee8 100644 --- a/solr/modules/scripting/gradle.lockfile +++ b/solr/modules/scripting/gradle.lockfile @@ -33,6 +33,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,runtimeLibs,solrPlatfor commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/modules/sql/gradle.lockfile b/solr/modules/sql/gradle.lockfile index 57f77bdfe53d..bcb290230a93 100644 --- a/solr/modules/sql/gradle.lockfile +++ b/solr/modules/sql/gradle.lockfile @@ -36,6 +36,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,runtimeLibs,solrPlatfor commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/server/gradle.lockfile b/solr/server/gradle.lockfile index 9d5d42b6176a..b4e77c309a22 100644 --- a/solr/server/gradle.lockfile +++ b/solr/server/gradle.lockfile @@ -32,6 +32,7 @@ com.tdunning:t-digest:3.3=jarValidation,runtimeClasspath,solrCore commons-cli:commons-cli:1.10.0=jarValidation,runtimeClasspath,solrCore commons-codec:commons-codec:1.19.0=jarValidation,runtimeClasspath,solrCore commons-io:commons-io:2.20.0=jarValidation,runtimeClasspath,solrCore +info.picocli:picocli:4.7.6=jarValidation,runtimeClasspath,solrCore io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,runtimeClasspath,solrCore io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor diff --git a/solr/solr-ref-guide/gradle.lockfile b/solr/solr-ref-guide/gradle.lockfile index 52404fd69b42..77bb2f2cb0a7 100644 --- a/solr/solr-ref-guide/gradle.lockfile +++ b/solr/solr-ref-guide/gradle.lockfile @@ -33,6 +33,7 @@ com.tdunning:t-digest:3.3=testRuntimeClasspath commons-cli:commons-cli:1.10.0=testRuntimeClasspath commons-codec:commons-codec:1.19.0=testRuntimeClasspath commons-io:commons-io:2.20.0=testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=testRuntimeClasspath diff --git a/solr/solrj-jetty/gradle.lockfile b/solr/solrj-jetty/gradle.lockfile index 862a0a52fd64..c8007848a888 100644 --- a/solr/solrj-jetty/gradle.lockfile +++ b/solr/solrj-jetty/gradle.lockfile @@ -33,6 +33,7 @@ com.tdunning:t-digest:3.3=jarValidation,testRuntimeClasspath commons-cli:commons-cli:1.10.0=jarValidation,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,testRuntimeClasspath commons-io:commons-io:2.20.0=jarValidation,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/solrj-streaming/gradle.lockfile b/solr/solrj-streaming/gradle.lockfile index d88dd24a13f4..2c0e91d0e14d 100644 --- a/solr/solrj-streaming/gradle.lockfile +++ b/solr/solrj-streaming/gradle.lockfile @@ -33,6 +33,7 @@ com.tdunning:t-digest:3.3=jarValidation,testRuntimeClasspath commons-cli:commons-cli:1.10.0=jarValidation,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,testRuntimeClasspath commons-io:commons-io:2.20.0=jarValidation,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/solrj-zookeeper/gradle.lockfile b/solr/solrj-zookeeper/gradle.lockfile index 687e58c37caa..298846a0a5cc 100644 --- a/solr/solrj-zookeeper/gradle.lockfile +++ b/solr/solrj-zookeeper/gradle.lockfile @@ -38,6 +38,7 @@ commons-cli:commons-cli:1.10.0=jarValidation,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,testRuntimeClasspath commons-io:commons-io:2.17.0=apiHelper commons-io:commons-io:2.20.0=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/solrj/gradle.lockfile b/solr/solrj/gradle.lockfile index 363bc0de923d..c90961febe87 100644 --- a/solr/solrj/gradle.lockfile +++ b/solr/solrj/gradle.lockfile @@ -39,6 +39,7 @@ com.tdunning:t-digest:3.3=jarValidation,testRuntimeClasspath commons-cli:commons-cli:1.10.0=jarValidation,testRuntimeClasspath commons-codec:commons-codec:1.19.0=jarValidation,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=jarValidation,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/test-framework/gradle.lockfile b/solr/test-framework/gradle.lockfile index 2ae0de12ce0c..b97f50c3595d 100644 --- a/solr/test-framework/gradle.lockfile +++ b/solr/test-framework/gradle.lockfile @@ -34,6 +34,7 @@ com.tdunning:t-digest:3.3=apiHelper,jarValidation,runtimeClasspath,testRuntimeCl commons-cli:commons-cli:1.10.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-codec:commons-codec:1.19.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/solr/webapp/gradle.lockfile b/solr/webapp/gradle.lockfile index ea94512eff69..fcd5f5c40905 100644 --- a/solr/webapp/gradle.lockfile +++ b/solr/webapp/gradle.lockfile @@ -32,6 +32,7 @@ com.tdunning:t-digest:3.3=solrCore commons-cli:commons-cli:1.10.0=solrCore commons-codec:commons-codec:1.19.0=solrCore commons-io:commons-io:2.20.0=solrCore +info.picocli:picocli:4.7.6=solrCore io.dropwizard.metrics:metrics-core:4.2.26=solrCore io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor From 3cbac7428792e28833ee7dad522719af553e0bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sun, 5 Apr 2026 14:49:44 +0200 Subject: [PATCH 02/19] Add sha and license for picocli --- solr/licenses/picocli-4.7.6.jar.sha1 | 1 + solr/licenses/picocli-LICENSE-ASL.txt | 201 ++++++++++++++++++++++++++ solr/licenses/picocli-NOTICE.txt | 2 + 3 files changed, 204 insertions(+) create mode 100644 solr/licenses/picocli-4.7.6.jar.sha1 create mode 100644 solr/licenses/picocli-LICENSE-ASL.txt create mode 100644 solr/licenses/picocli-NOTICE.txt diff --git a/solr/licenses/picocli-4.7.6.jar.sha1 b/solr/licenses/picocli-4.7.6.jar.sha1 new file mode 100644 index 000000000000..c63f30646a6d --- /dev/null +++ b/solr/licenses/picocli-4.7.6.jar.sha1 @@ -0,0 +1 @@ +77c2cb87814b6a03d431fc856024a9f8ff605ad4 diff --git a/solr/licenses/picocli-LICENSE-ASL.txt b/solr/licenses/picocli-LICENSE-ASL.txt new file mode 100644 index 000000000000..9c8f3ea0871e --- /dev/null +++ b/solr/licenses/picocli-LICENSE-ASL.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed 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. \ No newline at end of file diff --git a/solr/licenses/picocli-NOTICE.txt b/solr/licenses/picocli-NOTICE.txt new file mode 100644 index 000000000000..d77f6f044d44 --- /dev/null +++ b/solr/licenses/picocli-NOTICE.txt @@ -0,0 +1,2 @@ +Picocli is licensed under the Apache License, Version 2.0. +Copyright (c) 2015-2025 Remko Popma and contributors. \ No newline at end of file From cb41a6944445544423fed35c99e2da3d40ad9936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sun, 5 Apr 2026 14:49:59 +0200 Subject: [PATCH 03/19] Add changelog entry for SOLR-17697 --- changelog/unreleased/SOLR-17697-picocli.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 changelog/unreleased/SOLR-17697-picocli.yml diff --git a/changelog/unreleased/SOLR-17697-picocli.yml b/changelog/unreleased/SOLR-17697-picocli.yml new file mode 100644 index 000000000000..23fd96984011 --- /dev/null +++ b/changelog/unreleased/SOLR-17697-picocli.yml @@ -0,0 +1,8 @@ +title: Add picocli as a dependency to enable migration of CLI tools away from commons-cli +type: other +authors: + - name: Jan Høydahl + url: https://home.apache.org/phonebook.html?uid=janhoy +links: + - name: SOLR-17697 + url: https://issues.apache.org/jira/browse/SOLR-17697 From 79feaf534c9fdd9a519e7627964c0fb35223df90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Sun, 5 Apr 2026 22:33:06 +0200 Subject: [PATCH 04/19] Enable picocli for start, stop, status, version and zk ls --- gradle/java/javac.gradle | 3 +- gradle/libs.versions.toml | 1 + solr/bin/solr | 6 +- solr/bin/solr.cmd | 18 +- solr/core/build.gradle | 2 +- solr/core/gradle.lockfile | 3 +- .../src/java/org/apache/solr/cli/ApiTool.java | 5 + .../java/org/apache/solr/cli/AssertTool.java | 5 + .../java/org/apache/solr/cli/AuthTool.java | 5 + .../java/org/apache/solr/cli/CLIUtils.java | 1 + .../solr/cli/CliDefaultValueProvider.java | 78 ++++++ .../java/org/apache/solr/cli/ClusterTool.java | 5 + .../solr/cli/ConfigSetDownloadTool.java | 5 + .../apache/solr/cli/ConfigSetUploadTool.java | 5 + .../java/org/apache/solr/cli/ConfigTool.java | 5 + .../java/org/apache/solr/cli/CreateTool.java | 5 + .../java/org/apache/solr/cli/DeleteTool.java | 5 + .../java/org/apache/solr/cli/ExportTool.java | 5 + .../org/apache/solr/cli/HealthcheckTool.java | 5 + .../java/org/apache/solr/cli/PackageTool.java | 5 + .../org/apache/solr/cli/PostLogsTool.java | 5 + .../java/org/apache/solr/cli/PostTool.java | 5 + .../org/apache/solr/cli/RunExampleTool.java | 5 + .../apache/solr/cli/SnapshotCreateTool.java | 5 + .../apache/solr/cli/SnapshotDeleteTool.java | 5 + .../apache/solr/cli/SnapshotDescribeTool.java | 5 + .../apache/solr/cli/SnapshotExportTool.java | 5 + .../org/apache/solr/cli/SnapshotListTool.java | 5 + .../src/java/org/apache/solr/cli/SolrCLI.java | 105 +++++++- .../org/apache/solr/cli/StartCommand.java | 228 ++++++++++++++++++ .../java/org/apache/solr/cli/StatusTool.java | 120 ++++++--- .../java/org/apache/solr/cli/StopCommand.java | 50 ++++ .../java/org/apache/solr/cli/StreamTool.java | 5 + .../src/java/org/apache/solr/cli/Tool.java | 6 + .../java/org/apache/solr/cli/ToolBase.java | 41 +++- .../org/apache/solr/cli/UpdateACLTool.java | 5 + .../java/org/apache/solr/cli/VersionTool.java | 15 ++ .../apache/solr/cli/ZkConnectionOptions.java | 100 ++++++++ .../java/org/apache/solr/cli/ZkCpTool.java | 5 + .../java/org/apache/solr/cli/ZkLsTool.java | 53 +++- .../org/apache/solr/cli/ZkMkrootTool.java | 5 + .../java/org/apache/solr/cli/ZkMvTool.java | 5 + .../java/org/apache/solr/cli/ZkRmTool.java | 5 + .../src/java/org/apache/solr/cli/ZkTool.java | 40 +++ .../java/org/apache/solr/cli/ZkToolHelp.java | 5 + solr/test-framework/build.gradle | 4 + solr/test-framework/gradle.lockfile | 5 +- 47 files changed, 957 insertions(+), 57 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java create mode 100644 solr/core/src/java/org/apache/solr/cli/StartCommand.java create mode 100644 solr/core/src/java/org/apache/solr/cli/StopCommand.java create mode 100644 solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java create mode 100644 solr/core/src/java/org/apache/solr/cli/ZkTool.java diff --git a/gradle/java/javac.gradle b/gradle/java/javac.gradle index 8a7af8b04d70..3e992b1200ec 100644 --- a/gradle/java/javac.gradle +++ b/gradle/java/javac.gradle @@ -60,7 +60,8 @@ allprojects { "-Xlint:text-blocks", "-proc:none", // proc:none was added because of LOG4J2-1925 / JDK-8186647 "-Xlint:removal", - "--should-stop=ifError=FLOW" // error-prone 2.41 + "--should-stop=ifError=FLOW", // error-prone 2.41 + "-Aproject=${project.group}/${project.name}" ] if (propertyOrDefault("javac.failOnWarnings", true).toBoolean()) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9222bc94c01..4a7684d42713 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -502,6 +502,7 @@ ow2-asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "ow2-asm" } # @keep transitive dependency for version alignment perfmark-api = { module = "io.perfmark:perfmark-api", version.ref = "perfmark" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } +picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" } prometheus-metrics-expositionformats = { module = "io.prometheus:prometheus-metrics-exposition-formats", version.ref = "prometheus-metrics" } prometheus-metrics-model = { module = "io.prometheus:prometheus-metrics-model", version.ref = "prometheus-metrics" } prometheus-simpleclient = { module = "io.prometheus:simpleclient", version.ref = "prometheus-simpleclient" } diff --git a/solr/bin/solr b/solr/bin/solr index eaa417ca90d0..387e942ca5aa 100755 --- a/solr/bin/solr +++ b/solr/bin/solr @@ -812,7 +812,11 @@ if [ $# -gt 0 ]; then shift 2 ;; -h|--help) - print_usage "$SCRIPT_CMD" + if [[ "${SOLR_PICOCLI:-}" == "true" ]]; then + run_tool "$SCRIPT_CMD" --help + else + print_usage "$SCRIPT_CMD" + fi exit 0 ;; -y|--no-prompt) diff --git a/solr/bin/solr.cmd b/solr/bin/solr.cmd index e30cc4416fa8..80097d5da307 100755 --- a/solr/bin/solr.cmd +++ b/solr/bin/solr.cmd @@ -414,8 +414,22 @@ IF "%1"=="--all" goto set_stop_all :parse_general_args REM Print usage of command in case help option included -IF "%1"=="--help" goto usage -IF "%1"=="-h" goto usage +IF "%1"=="--help" goto check_picocli_help +IF "%1"=="-h" goto check_picocli_help +goto after_help_check + +:check_picocli_help +IF "%SOLR_PICOCLI%"=="true" goto run_picocli_help +goto usage + +:run_picocli_help +"%JAVA%" %SOLR_SSL_OPTS% %AUTHC_OPTS% %SOLR_ZK_CREDS_AND_ACLS% %SOLR_TOOL_OPTS% -Dsolr.install.dir="%SOLR_TIP%" ^ + -Dlog4j.configurationFile="file:///%DEFAULT_SERVER_DIR%\resources\log4j2-console.xml" ^ + -classpath "%SOLR_TIP%\lib\*;%DEFAULT_SERVER_DIR%\solr-webapp\webapp\WEB-INF\lib\*;%DEFAULT_SERVER_DIR%\lib\ext\*" ^ + org.apache.solr.cli.SolrCLI %SCRIPT_CMD% --help +goto done + +:after_help_check REM other args supported by all special commands IF "%1"=="-p" goto set_port diff --git a/solr/core/build.gradle b/solr/core/build.gradle index 144a7445f9c7..398a0a2a2ca4 100644 --- a/solr/core/build.gradle +++ b/solr/core/build.gradle @@ -99,7 +99,7 @@ dependencies { implementation libs.commonscli.commonscli implementation libs.picocli - permitUnusedDeclared libs.picocli // will be used when CLI is migrated to picocli + annotationProcessor libs.picocli.codegen implementation libs.locationtech.spatial4j diff --git a/solr/core/gradle.lockfile b/solr/core/gradle.lockfile index ec0442ee4662..512297f3c9a9 100644 --- a/solr/core/gradle.lockfile +++ b/solr/core/gradle.lockfile @@ -37,7 +37,8 @@ com.tdunning:t-digest:3.3=compileClasspath,jarValidation,runtimeClasspath,runtim commons-cli:commons-cli:1.10.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath commons-codec:commons-codec:1.19.0=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath -info.picocli:picocli:4.7.6=compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli-codegen:4.7.6=annotationProcessor +info.picocli:picocli:4.7.6=annotationProcessor,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=jarValidation,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=jarValidation,testRuntimeClasspath diff --git a/solr/core/src/java/org/apache/solr/cli/ApiTool.java b/solr/core/src/java/org/apache/solr/cli/ApiTool.java index 4a9c86fb8485..b5679429d1ac 100644 --- a/solr/core/src/java/org/apache/solr/cli/ApiTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ApiTool.java @@ -117,4 +117,9 @@ public static ModifiableSolrParams getSolrParamsFromUri(URI uri) { } return paramsMap; } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/AssertTool.java b/solr/core/src/java/org/apache/solr/cli/AssertTool.java index 928065f0a798..064545f9fd3d 100644 --- a/solr/core/src/java/org/apache/solr/cli/AssertTool.java +++ b/solr/core/src/java/org/apache/solr/cli/AssertTool.java @@ -426,6 +426,11 @@ private static boolean runningSolrIsCloud(String url, String credentials) throws } } + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } + public static class AssertionFailureException extends Exception { public AssertionFailureException(String message) { super(message); diff --git a/solr/core/src/java/org/apache/solr/cli/AuthTool.java b/solr/core/src/java/org/apache/solr/cli/AuthTool.java index 5c19ac300d7f..606086529180 100644 --- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java +++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java @@ -452,4 +452,9 @@ public void runImpl(CommandLine cli) throws Exception { throw new IllegalStateException("Only type=basicAuth supported at the moment."); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java index 5653bffa8970..48531f0ac16b 100644 --- a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java +++ b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java @@ -183,6 +183,7 @@ public static String normalizeSolrUrl(String solrUrl, boolean logUrlFormatWarnin * Get the base URL of a live Solr instance from either the --solr-url command-line option or from * ZooKeeper. */ + @Deprecated public static String normalizeSolrUrl(CommandLine cli) throws Exception { String solrUrl = cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION); diff --git a/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java b/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java new file mode 100644 index 000000000000..99cb2ab95564 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java @@ -0,0 +1,78 @@ +/* + * 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.cli; + +import static org.apache.solr.cli.CLIUtils.getCloudSolrClient; +import static org.apache.solr.cli.CLIUtils.normalizeSolrUrl; + +import java.util.Set; +import org.apache.solr.client.solrj.impl.CloudSolrClient; +import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.util.EnvUtils; +import picocli.CommandLine; + +/** Provides default values for CLI arguments. */ +public class CliDefaultValueProvider implements CommandLine.IDefaultValueProvider { + @Override + public String defaultValue(CommandLine.Model.ArgSpec argSpec) throws Exception { + return switch (argSpec.paramLabel()) { + case "" -> EnvUtils.getProperty("zkHost"); + case "" -> { + String val = EnvUtils.getProperty("solr.url"); + yield val != null ? val : resolveSolrUrlViaZkHost(argSpec); + } + case "" -> EnvUtils.getProperty("solr.port", "8983"); + case "" -> EnvUtils.getProperty("solr.max.wait.seconds", "0"); + default -> null; + }; + } + + /** + * If no solrUrl is provided on the command line, and SOLR_URL is not set, this method will be + * used to determine the solrUrl from the zkHost. + * + * @param argSpec the argSpec for the solrUrl option + * @return the solrUrl + * @throws Exception if an error occurs + */ + public static String resolveSolrUrlViaZkHost(picocli.CommandLine.Model.ArgSpec argSpec) + throws Exception { + // Find value of zkHost from command line options. The argSpec passed in will be for the + // solrUrl option. + CommandLine.Model.OptionSpec zkHostOption = argSpec.command().findOption("--zk-host"); + + String zkHost = zkHostOption != null ? zkHostOption.getValue() : null; + if (zkHost == null) { + return null; + } + + String solrUrl; + try (CloudSolrClient cloudSolrClient = getCloudSolrClient(zkHost)) { + cloudSolrClient.connect(); + Set liveNodes = cloudSolrClient.getClusterState().getLiveNodes(); + if (liveNodes.isEmpty()) + throw new IllegalStateException( + "No live nodes found! Cannot determine 'solrUrl' from ZooKeeper: " + zkHost); + + String firstLiveNode = liveNodes.iterator().next(); + solrUrl = ZkStateReader.from(cloudSolrClient).getBaseUrlForNodeName(firstLiveNode); + solrUrl = normalizeSolrUrl(solrUrl, false); + } + solrUrl = normalizeSolrUrl(solrUrl); + return solrUrl; + } +} diff --git a/solr/core/src/java/org/apache/solr/cli/ClusterTool.java b/solr/core/src/java/org/apache/solr/cli/ClusterTool.java index 54626e2b7db7..aa70d0582fef 100644 --- a/solr/core/src/java/org/apache/solr/cli/ClusterTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ClusterTool.java @@ -97,4 +97,9 @@ public void runImpl(CommandLine cli) throws Exception { } } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java index 5623794626cb..a20c5bff853f 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java @@ -101,4 +101,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java index fdd2380f3a19..ba3f188b0b9a 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java @@ -107,4 +107,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigTool.java index a4145168165b..b2a340ba7ed2 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigTool.java @@ -137,4 +137,9 @@ public void runImpl(CommandLine cli) throws Exception { } } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java b/solr/core/src/java/org/apache/solr/cli/CreateTool.java index 74778e7e4904..1f81609f0b78 100644 --- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java @@ -352,4 +352,9 @@ private void printDefaultConfigsetWarningIfNecessary(CommandLine cli) { echo(" " + configCommand); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java index 77db2184a4cb..07f92894f311 100644 --- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java @@ -216,4 +216,9 @@ protected void deleteCore(CommandLine cli, SolrClient solrClient) throws Excepti throw new Exception("Failed to delete core '" + coreName + "' due to: " + sse.getMessage()); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ExportTool.java b/solr/core/src/java/org/apache/solr/cli/ExportTool.java index 6fd43b52a1c8..e95af8640baf 100644 --- a/solr/core/src/java/org/apache/solr/cli/ExportTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ExportTool.java @@ -707,4 +707,9 @@ static long getDocCount(String coreName, SolrClient client, String query) SolrDocumentList sdl = (SolrDocumentList) res.get("response"); return sdl.getNumFound(); } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java index 670bb0eb6563..2eea27280936 100644 --- a/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java +++ b/solr/core/src/java/org/apache/solr/cli/HealthcheckTool.java @@ -213,6 +213,11 @@ protected void runCloudTool(CloudSolrClient cloudSolrClient, CommandLine cli) th new JSONWriter(arr, 2).write(report); echo(arr.toString()); } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } class ReplicaHealth implements Comparable { diff --git a/solr/core/src/java/org/apache/solr/cli/PackageTool.java b/solr/core/src/java/org/apache/solr/cli/PackageTool.java index aaa3649d1be8..9dea8aaba9e9 100644 --- a/solr/core/src/java/org/apache/solr/cli/PackageTool.java +++ b/solr/core/src/java/org/apache/solr/cli/PackageTool.java @@ -378,4 +378,9 @@ public Options getOptions() { .addOption(CommonCLIOptions.CREDENTIALS_OPTION) .addOptionGroup(getConnectionOptions()); } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java index 82dccf537427..64a99784dacb 100644 --- a/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java +++ b/solr/core/src/java/org/apache/solr/cli/PostLogsTool.java @@ -626,4 +626,9 @@ public static String[] getRequestPurposeNames(Integer reqPurpose) { map.put(ShardRequest.PURPOSE_GET_TERM_STATS, "GET_TERM_STATS"); purposes = Collections.unmodifiableMap(map); } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/PostTool.java b/solr/core/src/java/org/apache/solr/cli/PostTool.java index 97751490e3f4..bf8832ad6785 100644 --- a/solr/core/src/java/org/apache/solr/cli/PostTool.java +++ b/solr/core/src/java/org/apache/solr/cli/PostTool.java @@ -1342,6 +1342,11 @@ protected Set getLinksFromWebPage(URL url, InputStream is, String type, URI } } + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } + /** Utility class to hold the result form a page fetch */ public static class PageFetcherResult { int httpStatus = 200; diff --git a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java index e1d002cf21ee..d6c120be041f 100644 --- a/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java +++ b/solr/core/src/java/org/apache/solr/cli/RunExampleTool.java @@ -1079,6 +1079,11 @@ protected void copyIfNeeded(Path src, Path dest) throws IOException { throw new IllegalStateException("Required file " + dest.toAbsolutePath() + " not found!"); } + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } + protected boolean isPortAvailable(int port) { try (Socket s = new Socket("localhost", port)) { assert s != null; // To allow compilation. diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java b/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java index dfc39bf7cb2d..893d57e4c6c7 100644 --- a/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/SnapshotCreateTool.java @@ -97,4 +97,9 @@ public void createSnapshot(SolrClient solrClient, String collectionName, String + e.getLocalizedMessage()); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java b/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java index 00b5c3c01979..5092d87b68f6 100644 --- a/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/SnapshotDeleteTool.java @@ -97,4 +97,9 @@ public void deleteSnapshot(SolrClient solrClient, String collectionName, String + e.getLocalizedMessage()); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java b/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java index 477ad265e7d5..3b5102d50796 100644 --- a/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java +++ b/solr/core/src/java/org/apache/solr/cli/SnapshotDescribeTool.java @@ -136,4 +136,9 @@ private Collection listCollectionSnapshots( return result; } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java b/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java index d71b8df8b1a2..6b2406fd80e2 100644 --- a/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java +++ b/solr/core/src/java/org/apache/solr/cli/SnapshotExportTool.java @@ -131,4 +131,9 @@ public void exportSnapshot( + e.getLocalizedMessage()); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java b/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java index 4501fddc8397..eedad418a1c0 100644 --- a/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java +++ b/solr/core/src/java/org/apache/solr/cli/SnapshotListTool.java @@ -85,4 +85,9 @@ public void listSnapshots(SolrClient solrClient, String collectionName) { + e.getLocalizedMessage()); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index 9f34c32cb371..6a6828161ff9 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -42,9 +42,11 @@ import org.apache.commons.cli.help.HelpFormatter; import org.apache.commons.cli.help.TableDefinition; import org.apache.commons.cli.help.TextHelpAppendable; +import org.apache.solr.client.api.util.SolrVersion; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest; import org.apache.solr.common.util.ContentStreamBase; +import org.apache.solr.common.util.EnvUtils; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.SuppressForbidden; import org.apache.solr.util.configuration.SSLConfigurationsFactory; @@ -52,14 +54,92 @@ import org.slf4j.LoggerFactory; /** Command-line utility for working with Solr. */ +@picocli.CommandLine.Command( + name = "solr", + version = "Apache Solr version " + SolrVersion.LATEST_STRING, + mixinStandardHelpOptions = true, + commandListHeading = "\nCommands:\n", + descriptionHeading = "Global options:\n", + footer = { + "", + "SolrCloud example (embedded Zookeeper):", + "", + " ./solr start", + "", + "Get help for a command by running 'solr COMMAND --help'.", + "", + "For more help on how to use Solr, head to https://solr.apache.org/" + }, + usageHelpAutoWidth = true, + usageHelpWidth = 120, + defaultValueProvider = CliDefaultValueProvider.class, + subcommands = { + StartCommand.class, + StopCommand.class, + StatusTool.class, + VersionTool.class, + ZkTool.class + }) public class SolrCLI implements CLIO { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + @SuppressForbidden(reason = "SolrCLI is a CLI entry point; System.exit is required here") + public static void exit(int exitStatus) { + try { + System.exit(exitStatus); + } catch (java.lang.SecurityException secExc) { + if (exitStatus != 0) + throw new RuntimeException("SolrCLI failed to exit with status " + exitStatus); + } + } + /** Runs a tool. */ public static void main(String[] args) throws Exception { - ToolRuntime runtime = new DefaultToolRuntime(); + if (EnvUtils.getPropertyAsBool("solr.picocli", false)) { + SSLConfigurationsFactory.current().init(); + picocli.CommandLine commandLine = new picocli.CommandLine(new SolrCLI()); + propagateCommandSettings(commandLine); + exit(commandLine.execute(args)); + } else { + exit(parseWithCommonsCli(args)); + } + } + + /** Propagates common settings to all subcommands. */ + private static void propagateCommandSettings(picocli.CommandLine cmd) { + for (picocli.CommandLine subcommand : cmd.getSubcommands().values()) { + subcommand.getCommandSpec().defaultValueProvider(cmd.getCommandSpec().defaultValueProvider()); + subcommand + .getCommandSpec() + .mixinStandardHelpOptions(cmd.getCommandSpec().mixinStandardHelpOptions()); + subcommand.getCommandSpec().usageMessage().width(cmd.getCommandSpec().usageMessage().width()); + subcommand + .getCommandSpec() + .usageMessage() + .autoWidth(cmd.getCommandSpec().usageMessage().autoWidth()); + subcommand + .getCommandSpec() + .usageMessage() + .commandListHeading(cmd.getCommandSpec().usageMessage().commandListHeading()); + subcommand + .getCommandSpec() + .usageMessage() + .footer( + "\nFor a full CLI reference, see https://solr.apache.org/guide/solr/latest/deployment-guide/solr-control-script-reference.html"); + propagateCommandSettings(subcommand); + } + } + /** + * Parses the command-line arguments passed by the user using Apache Commons CLI. This + * + * @param args the original command-line arguments + * @deprecated Please use picocli + */ + @Deprecated(since = "10.1") + public static int parseWithCommonsCli(String[] args) throws Exception { + ToolRuntime runtime = new DefaultToolRuntime(); final boolean hasNoCommand = args == null || args.length == 0 || args[0] == null || args[0].trim().isEmpty(); final boolean isHelpCommand = !hasNoCommand && Arrays.asList("-h", "--help").contains(args[0]); @@ -114,7 +194,7 @@ public static void main(String[] args) throws Exception { runtime.exit(1); } CommandLine cli = parseCmdLine(tool, args); - runtime.exit(tool.runTool(cli)); + return tool.runTool(cli); } public static Tool findTool(String[] args, ToolRuntime runtime) throws Exception { @@ -122,6 +202,7 @@ public static Tool findTool(String[] args, ToolRuntime runtime) throws Exception return newTool(toolType, runtime); } + @Deprecated public static CommandLine parseCmdLine(Tool tool, String[] args) throws IOException { // the parser doesn't like -D props List toolArgList = new ArrayList<>(); @@ -137,7 +218,7 @@ public static CommandLine parseCmdLine(Tool tool, String[] args) throws IOExcept String[] toolArgs = toolArgList.toArray(new String[0]); // process command-line args to configure this application - CommandLine cli = processCommandLineArgs(tool, toolArgs); + org.apache.commons.cli.CommandLine cli = processCommandLineArgs(tool, toolArgs); List argList = cli.getArgList(); argList.addAll(dashDList); @@ -176,6 +257,7 @@ protected static void checkSslStoreSysProp(String solrInstallDir, String key) { } // Creates an instance of the requested tool, using classpath scanning if necessary + @Deprecated private static Tool newTool(String toolType, ToolRuntime runtime) throws Exception { if ("healthcheck".equals(toolType)) return new HealthcheckTool(runtime); else if ("status".equals(toolType)) return new StatusTool(runtime); @@ -228,7 +310,7 @@ private static Tool newTool(String toolType, ToolRuntime runtime) throws Excepti * CLI option. */ public static String getOptionWithDeprecatedAndDefault( - CommandLine cli, Option opt, Option deprecated, String def) { + org.apache.commons.cli.CommandLine cli, Option opt, Option deprecated, String def) { String val = cli.getOptionValue(opt); if (val == null) { val = cli.getOptionValue(deprecated); @@ -238,6 +320,7 @@ public static String getOptionWithDeprecatedAndDefault( // TODO: SOLR-17429 - remove the custom logic when Commons CLI is upgraded and // makes stderr the default, or makes Option.toDeprecatedString() public. + @Deprecated private static void deprecatedHandlerStdErr(Option o) { // Deprecated options without a description act as "stealth" options if (o.isDeprecated() && !o.getDeprecated().getDescription().isBlank()) { @@ -252,11 +335,12 @@ private static void deprecatedHandlerStdErr(Option o) { } /** Parses the command-line arguments passed by the user. */ + @Deprecated public static CommandLine processCommandLineArgs(Tool tool, String[] args) throws IOException { Options options = tool.getOptions(); ToolRuntime runtime = tool.getRuntime(); - CommandLine cli = null; + org.apache.commons.cli.CommandLine cli = null; try { cli = DefaultParser.builder() @@ -293,6 +377,7 @@ public static CommandLine processCommandLineArgs(Tool tool, String[] args) throw } /** Prints tool help for a given tool */ + @Deprecated public static void printToolHelp(Tool tool) throws IOException { HelpFormatter formatter = getFormatter(); Options nonDeprecatedOptions = new Options(); @@ -311,6 +396,7 @@ public static void printToolHelp(Tool tool) throws IOException { autoGenerateUsage); } + @Deprecated @SuppressForbidden(reason = "System.out for formatting") public static HelpFormatter getFormatter() { TextHelpAppendable helpAppendable = @@ -346,7 +432,12 @@ public void appendParagraph(CharSequence paragraph) throws IOException { return formatter; } - /** Scans Jar files on the classpath for Tool implementations to activate. */ + /** + * Scans Jar files on the classpath for Tool implementations to activate. + * + * @deprecated With Picocli we no longer need to scan the classpath for Tool implementations? + */ + @Deprecated private static List> findToolClassesInPackage(String packageName) { List> toolClasses = new ArrayList<>(); try { @@ -370,6 +461,7 @@ private static List> findToolClassesInPackage(String packa return toolClasses; } + @Deprecated private static Set findClasses(String path, String packageName) throws Exception { Set classes = new TreeSet<>(); if (path.startsWith("file:") && path.contains("!")) { @@ -428,6 +520,7 @@ public static String uptime(long uptimeMs) { numSeconds); } + @Deprecated private static void printHelp() { print("Usage: solr COMMAND OPTIONS"); diff --git a/solr/core/src/java/org/apache/solr/cli/StartCommand.java b/solr/core/src/java/org/apache/solr/cli/StartCommand.java new file mode 100644 index 000000000000..b7c0b3f07c5b --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/StartCommand.java @@ -0,0 +1,228 @@ +/* + * 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.cli; + +import java.util.concurrent.Callable; +import picocli.CommandLine; + +@CommandLine.Command( + name = "start", + description = "Starts Solr in standalone or SolrCloud mode.", + mixinStandardHelpOptions = true) +public class StartCommand implements Callable { + + @CommandLine.Spec CommandLine.Model.CommandSpec spec; + + @CommandLine.Option( + names = {"-f", "--foreground"}, + description = + "Start Solr in foreground; default is background with logs to solr-PORT-console.log") + boolean foreground; + + @CommandLine.Option( + names = "--user-managed", + description = "Start Solr in standalone mode. Default is SolrCloud (ZooKeeper) mode.") + boolean userManaged; + + @CommandLine.Option(names = "--host", description = "Specify the hostname for this Solr instance") + String host; + + @CommandLine.Option( + names = {"-p", "--port"}, + description = + "Specify the Solr HTTP port; default is 8983. STOP_PORT=($SOLR_PORT-1000), RMI_PORT=($SOLR_PORT+10000)") + String port; + + @CommandLine.Option( + names = "--server-dir", + description = "Specify the Solr server directory; default is 'server'") + String serverDir; + + @CommandLine.Option( + names = {"-z", "--zk-host"}, + description = + "Zookeeper connection string; default is to start an embedded ZooKeeper on PORT+10000") + String zkHost; + + @CommandLine.Option( + names = {"-m", "--memory"}, + description = "Set JVM heap size, e.g., -m 4g sets -Xms4g -Xmx4g; default is 512m") + String memory; + + @CommandLine.Option( + names = "--solr-home", + description = + "Set solr.solr.home system property; default is 'server/solr'. Ignored in examples mode") + String solrHome; + + @CommandLine.Option( + names = "--data-home", + description = + "Set solr.data.home system property for index data storage; default is solr.solr.home") + String dataHome; + + @CommandLine.Option( + names = {"-e", "--example"}, + description = "Run an example: cloud, techproducts, schemaless, films") + String example; + + @CommandLine.Option( + names = "--jvm-opts", + description = "Additional JVM parameters, e.g., --jvm-opts \"-verbose:gc\"") + String jvmOpts; + + @CommandLine.Option( + names = {"-j", "--jettyconfig"}, + description = + "Additional Jetty parameters, e.g., -j \"--include-jetty-dir=/etc/jetty/custom/server/\"") + String jettyParams; + + @CommandLine.Option( + names = {"-y", "--no-prompt"}, + description = "Don't prompt for input; accept all defaults when running examples") + boolean noPrompt; + + @CommandLine.Option( + names = "--prompt-inputs", + description = + "Don't prompt for input; comma-delimited list of inputs to use when running examples that accept user input", + paramLabel = "") + String promptInputs; + + @CommandLine.Option( + names = "--example-dir", + description = + "Override the directory containing example configurations used when running examples with --example") + String exampleDir; + + @CommandLine.Option( + names = "--force", + description = "Override warning when attempting to start Solr as root user") + boolean force; + + @CommandLine.Option( + names = {"--verbose"}, + description = "Set log level to DEBUG (verbose); default is INFO") + boolean verbose; + + @CommandLine.Option( + names = {"--quiet", "-q"}, + description = "Set log level to WARN (quiet); default is INFO") + boolean quiet; + + @CommandLine.Option( + names = "--fullhelp", + description = "Print detailed help with full option descriptions", + hidden = true) + boolean fullhelp; + + @Override + public Integer call() { + if (fullhelp) { + printFullHelp(); + } + // Actual start logic is handled by the bin/solr shell script. + return 0; + } + + private void printFullHelp() { + CLIO.out( + """ + Usage: solr start [OPTIONS] + + Starts Solr in standalone or SolrCloud mode. + + Options: + -f, --foreground + Start Solr in foreground; default starts Solr in the background and sends + stdout / stderr to solr-PORT-console.log + + --user-managed + Start Solr in user managed aka standalone mode. + See: https://solr.apache.org/guide/solr/latest/deployment-guide/cluster-types.html + + --host + Specify the hostname for this Solr instance + + -p, --port + Specify the port to start the Solr HTTP listener on; default is 8983. + The specified port (SOLR_PORT) will also be used to determine the stop port: + STOP_PORT=($SOLR_PORT-1000) and JMX RMI listen port RMI_PORT=($SOLR_PORT+10000). + For instance, if you set -p 8985, then STOP_PORT=7985 and RMI_PORT=18985 + + --server-dir + Specify the Solr server directory; defaults to server + + -z, --zk-host + Zookeeper connection string; ignored in User Managed (--user-managed) mode. + If neither ZK_HOST is defined in solr.in.sh nor -z is specified, an embedded + ZooKeeper instance will be launched. + Set ZK_CREATE_CHROOT=true if your ZK host has a chroot path to create it automatically. + + -m, --memory + Sets the min (-Xms) and max (-Xmx) heap size for the JVM, e.g., -m 4g sets + -Xms4g -Xmx4g; by default, this script sets the heap size to 512m + + --solr-home + Sets the solr.solr.home system property; Solr will create core directories here. + Allows running multiple Solr instances on the same host while reusing the same + server directory. If set, the directory should contain a solr.xml file unless + solr.xml exists in ZooKeeper. Ignored when running examples (-e). + Default: server/solr + + --data-home + Sets the solr.data.home system property, where Solr stores index data in + /data subdirectories. If not set, Solr uses solr.solr.home. + + -e, --example + Name of the example to run; available examples: + cloud: SolrCloud example + techproducts: Comprehensive example illustrating many of Solr's core capabilities + schemaless: Schema-less example (schema inferred from data during indexing) + films: Example starting with _default configset with explicit fields + + --jvm-opts + Additional parameters to pass to the JVM when starting Solr, e.g., to enable + a Java debugger: --jvm-opts "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=18983" + Wrap additional parameters in double quotes. + + -j, --jettyconfig + Additional parameters to pass to Jetty, e.g., to add a config folder: + -j "--include-jetty-dir=/etc/jetty/custom/server/" + Wrap additional parameters in double quotes. + + -y, --no-prompt + Don't prompt for input; accept all defaults when running examples. + + --prompt-inputs + Don't prompt for input; comma-delimited list of inputs for examples that + accept user input. + + --example-dir + Override the directory containing example configurations. + + --force + Override warning when attempting to start Solr as the root user. + + --verbose + Set log level to DEBUG (verbose); default is INFO + + -q, --quiet + Set log level to WARN (quiet); default is INFO + """); + } +} diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index ca9391fca18b..bbe253a6887f 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -44,8 +44,39 @@ * *

Get the status of a Solr server. */ +@picocli.CommandLine.Command( + name = "status", + mixinStandardHelpOptions = true, + description = "Get the status of a Solr server.") public class StatusTool extends ToolBase { + @picocli.CommandLine.Option( + names = {"--max-wait-secs"}, + description = "Wait up to the specified number of seconds to see Solr running.") + private Integer maxWaitSecs; + @picocli.CommandLine.Option( + names = {"-p", "--port"}, + description = "Port on localhost to check status for") + private Integer port; + + @picocli.CommandLine.Option( + names = {"-s", "--solr-url"}, + description = "Base Solr URL, which can be used to determine the zk-host if that's not known") + private String solrUrl; + + @picocli.CommandLine.Option( + names = {"--short"}, + paramLabel = "short", + description = "Short format. Prints one URL per line for running instances") + private boolean shortFormat; + + @picocli.CommandLine.Option( + names = {"-u", "--credentials"}, + description = + "Credentials in the format username:password. Example: --credentials solr:SolrRocks") + private String credentials; + + @Deprecated private static final Option MAX_WAIT_SECS_OPTION = Option.builder() .longOpt("max-wait-secs") @@ -56,6 +87,7 @@ public class StatusTool extends ToolBase { .desc("Wait up to the specified number of seconds to see Solr running.") .get(); + @Deprecated public static final Option PORT_OPTION = Option.builder("p") .longOpt("port") @@ -65,6 +97,7 @@ public class StatusTool extends ToolBase { .desc("Port on localhost to check status for") .get(); + @Deprecated public static final Option SHORT_OPTION = Option.builder() .longOpt("short") @@ -74,6 +107,10 @@ public class StatusTool extends ToolBase { private final SolrProcessManager processMgr; + public StatusTool() { + this(new DefaultToolRuntime()); + } + public StatusTool(ToolRuntime runtime) { super(runtime); processMgr = new SolrProcessManager(); @@ -98,11 +135,15 @@ public Options getOptions() { @Override public void runImpl(CommandLine cli) throws Exception { - String solrUrl = cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION); - Integer port = cli.hasOption(PORT_OPTION) ? cli.getParsedOptionValue(PORT_OPTION) : null; - boolean shortFormat = cli.hasOption(SHORT_OPTION); - int maxWaitSecs = cli.getParsedOptionValue(MAX_WAIT_SECS_OPTION, 0); + solrUrl = cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION); + port = cli.hasOption(PORT_OPTION) ? cli.getParsedOptionValue(PORT_OPTION) : null; + shortFormat = cli.hasOption(SHORT_OPTION); + maxWaitSecs = cli.getParsedOptionValue(MAX_WAIT_SECS_OPTION, 0); + + runTool(); + } + public int runTool() throws Exception { if (solrUrl != null) { if (!URLUtil.hasScheme(solrUrl)) { CLIO.err("Invalid URL provided: " + solrUrl); @@ -113,14 +154,14 @@ public void runImpl(CommandLine cli) throws Exception { if (maxWaitSecs > 0) { // Used by Windows start script when starting Solr try { - waitForSolrUpAndPrintStatus(solrUrl, cli, maxWaitSecs); + waitForSolrUpAndPrintStatus(solrUrl); runtime.exit(0); } catch (Exception e) { CLIO.err(e.getMessage()); runtime.exit(1); } } else { - boolean running = printStatusFromRunningSolr(solrUrl, cli); + boolean running = printStatusFromRunningSolr(solrUrl); runtime.exit(running ? 0 : 1); } } @@ -135,7 +176,7 @@ public void runImpl(CommandLine cli) throws Exception { if (shortFormat) { CLIO.out(solrUrl); } else { - printProcessStatus(proc.get(), cli); + printProcessStatus(proc.get()); } runtime.exit(0); } @@ -148,7 +189,7 @@ public void runImpl(CommandLine cli) throws Exception { if (shortFormat) { CLIO.out(process.getLocalUrl()); } else { - printProcessStatus(process, cli); + printProcessStatus(process); } } } else { @@ -156,17 +197,16 @@ public void runImpl(CommandLine cli) throws Exception { CLIO.out("\nNo Solr nodes are running.\n"); } } + return 0; } - private void printProcessStatus(SolrProcess process, CommandLine cli) throws Exception { - int maxWaitSecs = cli.getParsedOptionValue(MAX_WAIT_SECS_OPTION, 0); - boolean shortFormat = cli.hasOption(SHORT_OPTION); + private void printProcessStatus(SolrProcess process) throws Exception { String pidUrl = process.getLocalUrl(); if (shortFormat) { CLIO.out(pidUrl); } else { if (maxWaitSecs > 0) { - waitForSolrUpAndPrintStatus(pidUrl, cli, maxWaitSecs); + waitForSolrUpAndPrintStatus(pidUrl); } else { CLIO.out( String.format( @@ -174,23 +214,22 @@ private void printProcessStatus(SolrProcess process, CommandLine cli) throws Exc "\nSolr process %s running on port %s", process.pid(), process.port())); - printStatusFromRunningSolr(pidUrl, cli); + printStatusFromRunningSolr(pidUrl); } } CLIO.out(""); } - public void waitForSolrUpAndPrintStatus(String solrUrl, CommandLine cli, int maxWaitSecs) - throws Exception { + public void waitForSolrUpAndPrintStatus(String pidUrl) throws Exception { int solrPort = -1; try { - solrPort = CLIUtils.portFromUrl(solrUrl); + solrPort = CLIUtils.portFromUrl(pidUrl); } catch (Exception e) { CLIO.err("Invalid URL provided, does not contain port"); runtime.exit(1); } echo("Waiting up to " + maxWaitSecs + " seconds to see Solr running on port " + solrPort); - boolean solrUp = waitForSolrUp(solrUrl, cli, maxWaitSecs); + boolean solrUp = waitForSolrUp(pidUrl); if (solrUp) { echo("Started Solr server on port " + solrPort + ". Happy searching!"); } else { @@ -202,35 +241,28 @@ public void waitForSolrUpAndPrintStatus(String solrUrl, CommandLine cli, int max /** * Wait for Solr to come online and return true if it does, false otherwise. * - * @param solrUrl the URL of the Solr server - * @param cli the command line options - * @param maxWaitSecs the maximum number of seconds to wait * @return true if Solr comes online, false otherwise */ - public boolean waitForSolrUp(String solrUrl, CommandLine cli, int maxWaitSecs) throws Exception { + public boolean waitForSolrUp(String pidUrl) throws Exception { try { - waitToSeeSolrUp( - solrUrl, - cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), - maxWaitSecs, - TimeUnit.SECONDS); + waitToSeeSolrUp(pidUrl, credentials, maxWaitSecs, TimeUnit.SECONDS); return true; } catch (TimeoutException timeout) { return false; } } - public boolean printStatusFromRunningSolr(String solrUrl, CommandLine cli) { + public boolean printStatusFromRunningSolr(String pidUrl) { String statusJson = null; try { - statusJson = statusFromRunningSolr(solrUrl, cli); + statusJson = statusFromRunningSolr(pidUrl); } catch (Exception e) { /* ignore */ } if (statusJson != null) { runtime.println(statusJson); } else { - CLIO.err("Solr at " + solrUrl + " not online."); + CLIO.err("Solr at " + pidUrl + " not online."); } return statusJson != null; } @@ -238,16 +270,13 @@ public boolean printStatusFromRunningSolr(String solrUrl, CommandLine cli) { /** * Get the status of a Solr server and responds with a JSON status string. * - * @param solrUrl the URL of the Solr server - * @param cli the command line options * @return the status of the Solr server or null if the server is not online * @throws Exception if there is an error getting the status */ - public String statusFromRunningSolr(String solrUrl, CommandLine cli) throws Exception { + public String statusFromRunningSolr(String pidUrl) throws Exception { try { CharArr arr = new CharArr(); - new JSONWriter(arr, 2) - .write(getStatus(solrUrl, cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))); + new JSONWriter(arr, 2).write(getStatus(pidUrl)); return arr.toString(); } catch (Exception exc) { if (CLIUtils.exceptionIsAuthRelated(exc)) { @@ -257,19 +286,23 @@ public String statusFromRunningSolr(String solrUrl, CommandLine cli) throws Exce // this is not actually an error from the tool as it's ok if Solr is not online. return null; } else { - throw new Exception("Failed to get system information from " + solrUrl + " due to: " + exc); + throw new Exception("Failed to get system information from " + pidUrl + " due to: " + exc); } } } + public Map waitToSeeSolrUp(String pidUrl) throws Exception { + return waitToSeeSolrUp(pidUrl, credentials, maxWaitSecs, TimeUnit.SECONDS); + } + @SuppressWarnings("BusyWait") public Map waitToSeeSolrUp( - String solrUrl, String credentials, long maxWait, TimeUnit unit) throws Exception { + String pidUrl, String credentials, long maxWait, TimeUnit unit) throws Exception { long timeout = System.nanoTime() + TimeUnit.NANOSECONDS.convert(maxWait, unit); while (System.nanoTime() < timeout) { try { - return getStatus(solrUrl, credentials); + return getStatus(pidUrl); } catch (Exception exc) { if (CLIUtils.exceptionIsAuthRelated(exc)) { throw exc; @@ -289,8 +322,12 @@ public Map waitToSeeSolrUp( + " seconds!"); } - public Map getStatus(String solrUrl, String credentials) throws Exception { - try (var solrClient = CLIUtils.getSolrClient(solrUrl, credentials)) { + public Map getStatus(String pidUrl) throws Exception { + return getStatus(pidUrl, credentials); + } + + public Map getStatus(String pidUrl, String credentials) throws Exception { + try (var solrClient = CLIUtils.getSolrClient(pidUrl, credentials)) { return reportStatus(solrClient); } } @@ -340,4 +377,9 @@ private static Map getCloudStatus(SolrClient solrClient, String return cloudStatus; } + + @Override + public int callTool() throws Exception { + return runTool(); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/StopCommand.java b/solr/core/src/java/org/apache/solr/cli/StopCommand.java new file mode 100644 index 000000000000..2dd4a762dfe0 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/StopCommand.java @@ -0,0 +1,50 @@ +/* + * 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.cli; + +import picocli.CommandLine; + +/** + * This class is currently only used for printing CLI usage. + * The stop logic is currently handled in start script. + */ +@CommandLine.Command(name = "stop", description = "Stops Solr.", mixinStandardHelpOptions = true) +public class StopCommand { + + @CommandLine.Option( + names = {"-p", "--port"}, + description = + "Specify the port the Solr HTTP listener is bound to.\n" + + "The STOP_PORT is derived as ($SOLR_PORT-1000).") + String port; + + @CommandLine.Option( + names = {"-k", "--key"}, + description = "Stop key; default is solrrocks", + defaultValue = "solrrocks") + String key; + + @CommandLine.Option( + names = "--all", + description = "Find and stop all running Solr servers on this host") + boolean all; + + @CommandLine.Option( + names = {"--verbose"}, + description = "Enable verbose mode.") + boolean verbose; +} diff --git a/solr/core/src/java/org/apache/solr/cli/StreamTool.java b/solr/core/src/java/org/apache/solr/cli/StreamTool.java index 61d95c79d418..a0830d9919a4 100644 --- a/solr/core/src/java/org/apache/solr/cli/StreamTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StreamTool.java @@ -480,6 +480,11 @@ static String listToString(List values, String internalDelim) { return buf.toString(); } + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } + static String readExpression(LineNumberReader bufferedReader, String[] args) throws IOException { StringBuilder exprBuff = new StringBuilder(); diff --git a/solr/core/src/java/org/apache/solr/cli/Tool.java b/solr/core/src/java/org/apache/solr/cli/Tool.java index 1ad58bce50e7..acaf9c62d31b 100644 --- a/solr/core/src/java/org/apache/solr/cli/Tool.java +++ b/solr/core/src/java/org/apache/solr/cli/Tool.java @@ -22,6 +22,7 @@ public interface Tool { /** Defines the interface to a Solr tool that can be run from this command-line app. */ + @Deprecated String getName(); /** @@ -30,6 +31,7 @@ public interface Tool { * * @return The custom usage string or 'null' to auto generate (default) */ + @Deprecated default String getUsage() { return null; } @@ -37,6 +39,7 @@ default String getUsage() { /** * Optional header to display before the options in help output. Defaults to 'List of options:' */ + @Deprecated default String getHeader() { return "List of options:"; } @@ -45,6 +48,7 @@ default String getHeader() { * Optional footer to display after the options in help output. Defaults to a link to reference * guide */ + @Deprecated default String getFooter() { return "\nPlease see the Reference Guide for more tools documentation: https://solr.apache.org/guide/solr/latest/deployment-guide/solr-control-script-reference.html"; } @@ -57,7 +61,9 @@ default String getFooter() { * * @return The {@link Options} this tool supports. */ + @Deprecated Options getOptions(); + @Deprecated int runTool(CommandLine cli) throws Exception; } diff --git a/solr/core/src/java/org/apache/solr/cli/ToolBase.java b/solr/core/src/java/org/apache/solr/cli/ToolBase.java index 9d3734c3d931..8b3d4304cf3c 100644 --- a/solr/core/src/java/org/apache/solr/cli/ToolBase.java +++ b/solr/core/src/java/org/apache/solr/cli/ToolBase.java @@ -18,18 +18,21 @@ package org.apache.solr.cli; import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.concurrent.Callable; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.solr.client.solrj.request.json.JacksonContentWriter; import org.apache.solr.util.StartupLoggingUtils; -public abstract class ToolBase implements Tool { +public abstract class ToolBase implements Tool, Callable { + @picocli.CommandLine.Option( + names = {"-v", "--verbose"}, + description = "Enable verbose mode.") + private boolean verbose = false; protected final ToolRuntime runtime; - private boolean verbose = false; - protected ToolBase(ToolRuntime runtime) { this.runtime = runtime; } @@ -75,6 +78,7 @@ public ToolRuntime getRuntime() { * * @return OptionGroup validates that only one option is supplied by the caller. */ + @Deprecated public OptionGroup getConnectionOptions() { OptionGroup optionGroup = new OptionGroup(); optionGroup.addOption(CommonCLIOptions.SOLR_URL_OPTION); @@ -112,5 +116,36 @@ private void raiseLogLevelUnlessVerbose() { } } + @Deprecated public abstract void runImpl(CommandLine cli) throws Exception; + + /** + * Called by picocli to execute the tool's logic. Each tool must implement this method to support + * the picocli-based invocation path. + */ + public abstract int callTool() throws Exception; + + /** Called by picocli for a tool invocation. Delegates to {@link #callTool()}. */ + @Override + public Integer call() { + raiseLogLevelUnlessVerbose(); + + int toolExitStatus = 0; + try { + toolExitStatus = callTool(); + } catch (Exception exc) { + // since this is a CLI, spare the user the stacktrace + String excMsg = exc.getMessage(); + if (excMsg != null) { + CLIO.err("\nERROR: " + excMsg + "\n"); + if (verbose) { + exc.printStackTrace(CLIO.getErrStream()); + } + toolExitStatus = 1; + } else { + throw new RuntimeException(exc); + } + } + return toolExitStatus; + } } diff --git a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java index 021f05d7e82c..0639a9ce9969 100644 --- a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java +++ b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java @@ -71,4 +71,9 @@ public void runImpl(CommandLine cli) throws Exception { zkClient.updateACLs(path); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/VersionTool.java b/solr/core/src/java/org/apache/solr/cli/VersionTool.java index b8ae826ebc6a..f8f8393dbde5 100644 --- a/solr/core/src/java/org/apache/solr/cli/VersionTool.java +++ b/solr/core/src/java/org/apache/solr/cli/VersionTool.java @@ -20,8 +20,13 @@ import org.apache.commons.cli.CommandLine; import org.apache.solr.client.api.util.SolrVersion; +@picocli.CommandLine.Command(name = "version", description = "Prints the Solr version.") public class VersionTool extends ToolBase { + public VersionTool() { + this(new DefaultToolRuntime()); + } + public VersionTool(ToolRuntime runtime) { super(runtime); } @@ -33,6 +38,16 @@ public String getName() { @Override public void runImpl(CommandLine cli) throws Exception { + printVersion(); + } + + @Override + public int callTool() throws Exception { + printVersion(); + return 0; + } + + private void printVersion() { CLIO.out("Solr version is: " + SolrVersion.LATEST); } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java b/solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java new file mode 100644 index 000000000000..432d8cddd03e --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java @@ -0,0 +1,100 @@ +/* + * 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.cli; + +import java.util.Map; +import org.apache.solr.client.solrj.SolrClient; +import picocli.CommandLine; + +/** + * Picocli mixin providing common ZooKeeper connection options shared across ZK sub-commands. + * + *

Use {@code @CommandLine.Mixin ZkConnectionOptions zkOpts} in a command class to inherit these + * options. Call {@link #resolveZkHost()} to obtain a resolved ZooKeeper connection string, applying + * the same fallback logic as the commons-cli path. + */ +public class ZkConnectionOptions { + + @CommandLine.Option( + names = {"-z", "--zk-host"}, + description = + "Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to " + + CommonCLIOptions.DefaultValues.ZK_HOST + + '.') + public String zkHost; + + @CommandLine.Option( + names = {"-s", "--solr-url"}, + description = + "Base Solr URL, which can be used to determine the zk-host if --zk-host is not known") + public String solrUrl; + + @CommandLine.Option( + names = {"-u", "--credentials"}, + description = + "Credentials in the format username:password. Example: --credentials solr:SolrRocks") + public String credentials; + + /** + * Resolves the ZooKeeper connection string using the following precedence: + * + *

    + *
  1. Explicit {@code --zk-host} option value + *
  2. ZooKeeper host derived by querying the Solr instance at {@code --solr-url} + *
  3. ZooKeeper host derived by querying the default Solr URL ({@code http://localhost:8983}), + * with a warning printed to stderr + *
+ * + * @return resolved ZooKeeper connection string, never null + * @throws IllegalStateException if the Solr instance is not running in SolrCloud mode + * @throws Exception if the Solr instance cannot be reached + */ + public String resolveZkHost() throws Exception { + if (zkHost != null) { + return zkHost; + } + + String resolvedSolrUrl = solrUrl; + if (resolvedSolrUrl == null) { + resolvedSolrUrl = CLIUtils.getDefaultSolrUrl(); + CLIO.err( + "Neither --zk-host or --solr-url parameters, nor ZK_HOST env var provided, so assuming solr url is " + + resolvedSolrUrl + + "."); + } + + try (SolrClient solrClient = CLIUtils.getSolrClient(resolvedSolrUrl, credentials)) { + Map status = StatusTool.reportStatus(solrClient); + @SuppressWarnings("unchecked") + Map cloud = (Map) status.get("cloud"); + if (cloud != null) { + String zookeeper = (String) cloud.get("ZooKeeper"); + if (zookeeper != null && zookeeper.endsWith("(embedded)")) { + zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length()); + } + if (zookeeper != null) { + return zookeeper; + } + } + } + + throw new IllegalStateException( + "Solr at " + + resolvedSolrUrl + + " is not running in SolrCloud mode. Cannot use zk commands."); + } +} diff --git a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java index 4bf9a2663156..3e7f912f2485 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java @@ -207,4 +207,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java index 2ca4be286f20..cd09844e05c1 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java @@ -17,16 +17,39 @@ package org.apache.solr.cli; import java.lang.invoke.MethodHandles; +import java.util.concurrent.TimeUnit; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; +import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; import org.apache.solr.common.cloud.SolrZkClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Supports zk ls command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "ls", + mixinStandardHelpOptions = true, + description = "List the contents of a ZooKeeper node.") public class ZkLsTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + + @picocli.CommandLine.Parameters( + index = "0", + arity = "1", + description = "The path of the ZooKeeper znode path to list.") + private String path; + + @picocli.CommandLine.Option( + names = {"-r", "--recursive"}, + description = "Apply the command recursively.") + private boolean recursive; + + public ZkLsTool() { + this(new DefaultToolRuntime()); + } + public ZkLsTool(ToolRuntime runtime) { super(runtime); } @@ -57,7 +80,6 @@ public void runImpl(CommandLine cli) throws Exception { try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); - boolean recursive = cli.hasOption(CommonCLIOptions.RECURSIVE_OPTION); echoIfVerbose( "Getting listing for ZooKeeper node " @@ -72,4 +94,33 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + private void doLs(SolrZkClient zkClient) throws Exception { + echoIfVerbose("\nConnecting to ZooKeeper at " + zkOpts.zkHost + " ..."); + echoIfVerbose( + "Getting listing for ZooKeeper node " + + path + + " from ZooKeeper at " + + zkOpts.zkHost + + " recursive: " + + recursive); + runtime.print(zkClient.listZnode(path, recursive)); + } + + @Override + public int callTool() throws Exception { + String zkHost = zkOpts.resolveZkHost(); + + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(zkHost) + .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + doLs(zkClient); + return 0; + } catch (Exception e) { + log.error("Could not complete ls operation for reason: ", e); + throw (e); + } + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java index 2a644f2a1364..5c0d51a30c19 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java @@ -85,4 +85,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java index a11ac0884f87..759eacb1b7b9 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java @@ -98,4 +98,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java index 92e6b4daaa4e..8aab732c6f8b 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java @@ -84,4 +84,9 @@ public void runImpl(CommandLine cli) throws Exception { throw (e); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkTool.java b/solr/core/src/java/org/apache/solr/cli/ZkTool.java new file mode 100644 index 000000000000..af69c2461e25 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/ZkTool.java @@ -0,0 +1,40 @@ +/* + * 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.cli; + +import java.util.concurrent.Callable; +import picocli.CommandLine; + +/** + * Sub commands for working with ZooKeeper, only here to provide a common parent for the subcommands + * and print tool help. + */ +@CommandLine.Command( + name = "zk", + mixinStandardHelpOptions = true, + description = "Sub commands for working with ZooKeeper.", + subcommands = {ZkLsTool.class}) +public class ZkTool implements Callable { + + @CommandLine.Spec CommandLine.Model.CommandSpec spec; + + @Override + public Integer call() { + spec.commandLine().usage(CLIO.getOutStream()); + return 0; + } +} diff --git a/solr/core/src/java/org/apache/solr/cli/ZkToolHelp.java b/solr/core/src/java/org/apache/solr/cli/ZkToolHelp.java index 9251484a859f..8217702f511f 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkToolHelp.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkToolHelp.java @@ -77,4 +77,9 @@ public void runImpl(CommandLine cli) throws Exception { "Pass --help or -h after any COMMAND to see command-specific usage information such as: ./solr zk ls --help"); } } + + @Override + public int callTool() throws Exception { + throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + } } diff --git a/solr/test-framework/build.gradle b/solr/test-framework/build.gradle index a7bac2b63bd8..78d2ed3592fe 100644 --- a/solr/test-framework/build.gradle +++ b/solr/test-framework/build.gradle @@ -67,6 +67,10 @@ dependencies { implementation libs.dropwizard.metrics.core implementation libs.dropwizard.metrics.jetty12.ee10 implementation libs.commonscli.commonscli + permitUnusedDeclared libs.commonscli.commonscli + implementation libs.picocli + permitUnusedDeclared libs.picocli + annotationProcessor libs.picocli.codegen implementation libs.apache.httpcomponents.httpclient implementation libs.apache.httpcomponents.httpcore implementation libs.opentelemetry.api diff --git a/solr/test-framework/gradle.lockfile b/solr/test-framework/gradle.lockfile index b97f50c3595d..6ff709116263 100644 --- a/solr/test-framework/gradle.lockfile +++ b/solr/test-framework/gradle.lockfile @@ -31,10 +31,11 @@ com.google.protobuf:protobuf-java:3.25.8=annotationProcessor,errorprone,testAnno com.j256.simplemagic:simplemagic:1.17=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath com.tdunning:t-digest:3.3=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath -commons-cli:commons-cli:1.10.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-cli:commons-cli:1.10.0=apiHelper,compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-codec:commons-codec:1.19.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.20.0=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -info.picocli:picocli:4.7.6=apiHelper,jarValidation,runtimeClasspath,testRuntimeClasspath +info.picocli:picocli-codegen:4.7.6=annotationProcessor +info.picocli:picocli:4.7.6=annotationProcessor,apiHelper,compileClasspath,jarValidation,permitUnusedDeclared,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-annotation:4.2.26=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-core:4.2.26=apiHelper,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.dropwizard.metrics:metrics-jetty12-ee10:4.2.26=compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath From 8c23c9fb578d677732d90d5af921a49e13a77114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Tue, 7 Apr 2026 01:19:30 +0200 Subject: [PATCH 05/19] SOLR-17697 Remove eager ZK connection from CliDefaultValueProvider (#4268) --- .../solr/cli/CliDefaultValueProvider.java | 46 +------------------ .../java/org/apache/solr/cli/StopCommand.java | 4 +- 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java b/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java index 99cb2ab95564..9362e070d817 100644 --- a/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java +++ b/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java @@ -16,12 +16,6 @@ */ package org.apache.solr.cli; -import static org.apache.solr.cli.CLIUtils.getCloudSolrClient; -import static org.apache.solr.cli.CLIUtils.normalizeSolrUrl; - -import java.util.Set; -import org.apache.solr.client.solrj.impl.CloudSolrClient; -import org.apache.solr.common.cloud.ZkStateReader; import org.apache.solr.common.util.EnvUtils; import picocli.CommandLine; @@ -31,48 +25,10 @@ public class CliDefaultValueProvider implements CommandLine.IDefaultValueProvide public String defaultValue(CommandLine.Model.ArgSpec argSpec) throws Exception { return switch (argSpec.paramLabel()) { case "" -> EnvUtils.getProperty("zkHost"); - case "" -> { - String val = EnvUtils.getProperty("solr.url"); - yield val != null ? val : resolveSolrUrlViaZkHost(argSpec); - } + case "" -> EnvUtils.getProperty("solr.url"); case "" -> EnvUtils.getProperty("solr.port", "8983"); case "" -> EnvUtils.getProperty("solr.max.wait.seconds", "0"); default -> null; }; } - - /** - * If no solrUrl is provided on the command line, and SOLR_URL is not set, this method will be - * used to determine the solrUrl from the zkHost. - * - * @param argSpec the argSpec for the solrUrl option - * @return the solrUrl - * @throws Exception if an error occurs - */ - public static String resolveSolrUrlViaZkHost(picocli.CommandLine.Model.ArgSpec argSpec) - throws Exception { - // Find value of zkHost from command line options. The argSpec passed in will be for the - // solrUrl option. - CommandLine.Model.OptionSpec zkHostOption = argSpec.command().findOption("--zk-host"); - - String zkHost = zkHostOption != null ? zkHostOption.getValue() : null; - if (zkHost == null) { - return null; - } - - String solrUrl; - try (CloudSolrClient cloudSolrClient = getCloudSolrClient(zkHost)) { - cloudSolrClient.connect(); - Set liveNodes = cloudSolrClient.getClusterState().getLiveNodes(); - if (liveNodes.isEmpty()) - throw new IllegalStateException( - "No live nodes found! Cannot determine 'solrUrl' from ZooKeeper: " + zkHost); - - String firstLiveNode = liveNodes.iterator().next(); - solrUrl = ZkStateReader.from(cloudSolrClient).getBaseUrlForNodeName(firstLiveNode); - solrUrl = normalizeSolrUrl(solrUrl, false); - } - solrUrl = normalizeSolrUrl(solrUrl); - return solrUrl; - } } diff --git a/solr/core/src/java/org/apache/solr/cli/StopCommand.java b/solr/core/src/java/org/apache/solr/cli/StopCommand.java index 2dd4a762dfe0..f9f3fc626280 100644 --- a/solr/core/src/java/org/apache/solr/cli/StopCommand.java +++ b/solr/core/src/java/org/apache/solr/cli/StopCommand.java @@ -19,8 +19,8 @@ import picocli.CommandLine; /** - * This class is currently only used for printing CLI usage. - * The stop logic is currently handled in start script. + * This class is currently only used for printing CLI usage. The stop logic is currently handled in + * start script. */ @CommandLine.Command(name = "stop", description = "Stops Solr.", mixinStandardHelpOptions = true) public class StopCommand { From bae63d4b1d647ec0dde19588373d87e9113d5b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Tue, 7 Apr 2026 16:25:48 +0200 Subject: [PATCH 06/19] SOLR-17697 Enable picocli for remaining zk sub commands (#4267) --- .../solr/cli/ConfigSetDownloadTool.java | 67 ++++++--- .../org/apache/solr/cli/ConfigSetOptions.java | 41 ++++++ .../apache/solr/cli/ConfigSetUploadTool.java | 73 +++++++--- .../org/apache/solr/cli/RecursiveOption.java | 31 +++++ .../src/java/org/apache/solr/cli/SolrCLI.java | 15 +- .../org/apache/solr/cli/UpdateACLTool.java | 40 +++++- .../java/org/apache/solr/cli/ZkCpTool.java | 51 ++++++- .../java/org/apache/solr/cli/ZkLsTool.java | 40 ++---- .../org/apache/solr/cli/ZkMkrootTool.java | 53 +++++++- .../java/org/apache/solr/cli/ZkMvTool.java | 89 +++++++++--- .../java/org/apache/solr/cli/ZkRmTool.java | 74 +++++++--- .../src/java/org/apache/solr/cli/ZkTool.java | 12 +- .../solr/cli/ZkSubcommandsPicocliTest.java | 50 +++++++ .../apache/solr/cli/ZkSubcommandsTest.java | 128 +++++++++++++++--- solr/packaging/build.gradle | 2 + solr/packaging/test/test_zk.bats | 15 +- 16 files changed, 629 insertions(+), 152 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/cli/ConfigSetOptions.java create mode 100644 solr/core/src/java/org/apache/solr/cli/RecursiveOption.java create mode 100644 solr/core/src/test/org/apache/solr/cli/ZkSubcommandsPicocliTest.java diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java index a20c5bff853f..aa5b3c91fac6 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java @@ -19,14 +19,20 @@ import java.lang.invoke.MethodHandles; import java.nio.file.Files; import java.nio.file.Path; +import java.util.concurrent.TimeUnit; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; +import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; import org.apache.solr.common.cloud.SolrZkClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Supports zk downconfig command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "downconfig", + mixinStandardHelpOptions = true, + description = "Download a configset from ZooKeeper to the local filesystem.") public class ConfigSetDownloadTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -48,6 +54,14 @@ public class ConfigSetDownloadTool extends ToolBase { .desc("Local directory with configs.") .get(); + @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + + @picocli.CommandLine.Mixin ConfigSetOptions configSetOpts; + + public ConfigSetDownloadTool() { + this(new DefaultToolRuntime()); + } + public ConfigSetDownloadTool(ToolRuntime runtime) { super(runtime); } @@ -74,36 +88,53 @@ public String getUsage() { @Override public void runImpl(CommandLine cli) throws Exception { String zkHost = CLIUtils.getZkHost(cli); - String confName = cli.getOptionValue(CONF_NAME_OPTION); String confDir = cli.getOptionValue(CONF_DIR_OPTION); echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { - Path configSetPath = Path.of(confDir); - // we try to be nice about having the "conf" in the directory, and we create it if it's not - // there. - if (!configSetPath.endsWith("/conf")) { - configSetPath = configSetPath.resolve("conf"); - } - Files.createDirectories(configSetPath); - echo( - "Downloading configset " - + confName - + " from ZooKeeper at " - + zkHost - + " to directory " - + configSetPath.toAbsolutePath()); - - zkClient.downConfig(confName, configSetPath); + doDownconfig(zkClient, zkHost, confName, confDir); } catch (Exception e) { log.error("Could not complete downconfig operation for reason: ", e); throw (e); } } + private void doDownconfig(SolrZkClient zkClient, String zkHost, String confName, String confDir) + throws Exception { + Path configSetPath = Path.of(confDir); + // we try to be nice about having the "conf" in the directory, and we create it if it's not + // there. + if (!configSetPath.endsWith("conf")) { + configSetPath = configSetPath.resolve("conf"); + } + Files.createDirectories(configSetPath); + echo( + "Downloading configset " + + confName + + " from ZooKeeper at " + + zkHost + + " to directory " + + configSetPath.toAbsolutePath()); + + zkClient.downConfig(confName, configSetPath); + } + @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + String zkHost = zkOpts.resolveZkHost(); + + echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(zkHost) + .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + doDownconfig(zkClient, zkHost, configSetOpts.confName, configSetOpts.confDir); + return 0; + } catch (Exception e) { + log.error("Could not complete downconfig operation for reason: ", e); + throw (e); + } } } diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetOptions.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetOptions.java new file mode 100644 index 000000000000..61eb657ff776 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetOptions.java @@ -0,0 +1,41 @@ +/* + * 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.cli; + +import picocli.CommandLine; + +/** + * Picocli mixin providing common configset options shared by upconfig and downconfig sub-commands. + * + *

Use {@code @CommandLine.Mixin ConfigSetOptions configSetOpts} in a command class to inherit + * these options. + */ +public class ConfigSetOptions { + + @CommandLine.Option( + names = {"-n", "--conf-name"}, + description = "Configset name in ZooKeeper.", + required = true) + public String confName; + + @CommandLine.Option( + names = {"-d", "--conf-dir"}, + description = "Local directory with configs.", + required = true, + paramLabel = "DIR") + public String confDir; +} diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java index ba3f188b0b9a..87bfd2fb87a4 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java @@ -18,9 +18,11 @@ import java.lang.invoke.MethodHandles; import java.nio.file.Path; +import java.util.concurrent.TimeUnit; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; +import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.cloud.ZkMaintenanceUtils; import org.apache.solr.core.ConfigSetService; @@ -29,6 +31,10 @@ import org.slf4j.LoggerFactory; /** Supports zk upconfig command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "upconfig", + mixinStandardHelpOptions = true, + description = "Upload a configset from the local filesystem to ZooKeeper.") public class ConfigSetUploadTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -50,6 +56,14 @@ public class ConfigSetUploadTool extends ToolBase { .desc("Local directory with configs.") .get(); + @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + + @picocli.CommandLine.Mixin ConfigSetOptions configSetOpts; + + public ConfigSetUploadTool() { + this(new DefaultToolRuntime()); + } + public ConfigSetUploadTool(ToolRuntime runtime) { super(runtime); } @@ -76,40 +90,55 @@ public String getUsage() { @Override public void runImpl(CommandLine cli) throws Exception { String zkHost = CLIUtils.getZkHost(cli); - - final String solrInstallDir = System.getProperty("solr.install.dir"); - Path solrInstallDirPath = Path.of(solrInstallDir); - String confName = cli.getOptionValue(CONF_NAME_OPTION); String confDir = cli.getOptionValue(CONF_DIR_OPTION); echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { - final Path configsetsDirPath = CLIUtils.getConfigSetsDir(solrInstallDirPath); - Path confPath = ConfigSetService.getConfigsetPath(confDir, configsetsDirPath.toString()); - - echo( - "Uploading " - + confPath.toAbsolutePath() - + " for config " - + confName - + " to ZooKeeper at " - + zkHost); - FileTypeMagicUtil.assertConfigSetFolderLegal(confPath); - ZkMaintenanceUtils.uploadToZK( - zkClient, - confPath, - ZkMaintenanceUtils.CONFIGS_ZKNODE + "/" + confName, - ZkMaintenanceUtils.UPLOAD_FILENAME_EXCLUDE_PATTERN); - + doUpconfig(zkClient, zkHost, confName, confDir); } catch (Exception e) { log.error("Could not complete upconfig operation for reason: ", e); throw (e); } } + private void doUpconfig(SolrZkClient zkClient, String zkHost, String confName, String confDir) + throws Exception { + final String solrInstallDir = System.getProperty("solr.install.dir"); + Path solrInstallDirPath = Path.of(solrInstallDir); + final Path configsetsDirPath = CLIUtils.getConfigSetsDir(solrInstallDirPath); + Path confPath = ConfigSetService.getConfigsetPath(confDir, configsetsDirPath.toString()); + + echo( + "Uploading " + + confPath.toAbsolutePath() + + " for config " + + confName + + " to ZooKeeper at " + + zkHost); + FileTypeMagicUtil.assertConfigSetFolderLegal(confPath); + ZkMaintenanceUtils.uploadToZK( + zkClient, + confPath, + ZkMaintenanceUtils.CONFIGS_ZKNODE + "/" + confName, + ZkMaintenanceUtils.UPLOAD_FILENAME_EXCLUDE_PATTERN); + } + @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + String zkHost = zkOpts.resolveZkHost(); + + echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(zkHost) + .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + doUpconfig(zkClient, zkHost, configSetOpts.confName, configSetOpts.confDir); + return 0; + } catch (Exception e) { + log.error("Could not complete upconfig operation for reason: ", e); + throw (e); + } } } diff --git a/solr/core/src/java/org/apache/solr/cli/RecursiveOption.java b/solr/core/src/java/org/apache/solr/cli/RecursiveOption.java new file mode 100644 index 000000000000..d6f9e3590b3a --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/RecursiveOption.java @@ -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.cli; + +import picocli.CommandLine; + +/** + * Picocli mixin providing the {@code --recursive} option shared by ZooKeeper sub-commands that + * support recursive traversal (cp, ls, rm). + */ +public class RecursiveOption { + + @CommandLine.Option( + names = {"-r", "--recursive"}, + description = "Apply the command recursively.") + public boolean recursive; +} diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index 6a6828161ff9..12e1ce518cb8 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -55,9 +55,10 @@ /** Command-line utility for working with Solr. */ @picocli.CommandLine.Command( - name = "solr", + name = "bin/solr", version = "Apache Solr version " + SolrVersion.LATEST_STRING, mixinStandardHelpOptions = true, + synopsisHeading = "usage: ", commandListHeading = "\nCommands:\n", descriptionHeading = "Global options:\n", footer = { @@ -66,7 +67,7 @@ "", " ./solr start", "", - "Get help for a command by running 'solr COMMAND --help'.", + "Get help for a command by running 'bin/solr COMMAND --help'.", "", "For more help on how to use Solr, head to https://solr.apache.org/" }, @@ -125,8 +126,14 @@ private static void propagateCommandSettings(picocli.CommandLine cmd) { subcommand .getCommandSpec() .usageMessage() - .footer( - "\nFor a full CLI reference, see https://solr.apache.org/guide/solr/latest/deployment-guide/solr-control-script-reference.html"); + .synopsisHeading(cmd.getCommandSpec().usageMessage().synopsisHeading()); + if (subcommand.getSubcommands().isEmpty()) { + subcommand + .getCommandSpec() + .usageMessage() + .footer( + "\nFor a full CLI reference, see https://solr.apache.org/guide/solr/latest/deployment-guide/solr-control-script-reference.html"); + } propagateCommandSettings(subcommand); } } diff --git a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java index 0639a9ce9969..cb79d0bf42c2 100644 --- a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java +++ b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java @@ -29,9 +29,25 @@ * *

Set ACL properties by directly manipulating ZooKeeper. */ +@picocli.CommandLine.Command( + name = "updateacls", + mixinStandardHelpOptions = true, + description = "Update ACLs for a ZooKeeper znode.") public class UpdateACLTool extends ToolBase { // It is a shame this tool doesn't more closely mimic how the ConfigTool works. + @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + + @picocli.CommandLine.Parameters( + index = "0", + arity = "1", + description = "The ZooKeeper znode path to update ACLs for.") + private String path; + + public UpdateACLTool() { + this(new DefaultToolRuntime()); + } + public UpdateACLTool(ToolRuntime runtime) { super(runtime); } @@ -53,7 +69,6 @@ public Options getOptions() { @Override public void runImpl(CommandLine cli) throws Exception { - String zkHost = CLIUtils.getZkHost(cli); String path = cli.getArgs()[0]; @@ -67,13 +82,30 @@ public void runImpl(CommandLine cli) throws Exception { .withUrl(zkHost) .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) .build()) { - - zkClient.updateACLs(path); + doUpdateAcls(zkClient, path); } } + private void doUpdateAcls(SolrZkClient zkClient, String path) throws Exception { + zkClient.updateACLs(path); + } + @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + String zkHost = zkOpts.resolveZkHost(); + + if (!ZkController.checkChrootPath(zkHost, true)) { + throw new IllegalStateException( + "A chroot was specified in zkHost but the znode doesn't exist."); + } + + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(zkHost) + .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + doUpdateAcls(zkClient, path); + return 0; + } } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java index 3e7f912f2485..e28a57715f6d 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java @@ -39,6 +39,18 @@ import org.slf4j.LoggerFactory; /** Supports zk cp command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "cp", + mixinStandardHelpOptions = true, + description = { + "Copy files or folders to/from ZooKeeper or ZooKeeper to ZooKeeper.", + "", + ", : [file:][/]path/to/local/file or zk:/path/to/zk/node", + "NOTE: and may both be ZooKeeper resources prefixed by 'zk:'", + "When is a zk resource, may be '.'", + "If ends with '/', then will be a local folder or parent znode", + "and the last element of the path will be appended unless also ends in a slash." + }) public class ZkCpTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -50,6 +62,32 @@ public class ZkCpTool extends ToolBase { .desc("Required to look up configuration for compressing state.json.") .get(); + @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + + @picocli.CommandLine.Parameters( + index = "0", + arity = "1", + description = "Source path: [file:][/]path/to/local/file or zk:/path/to/zk/node.") + private String src; + + @picocli.CommandLine.Parameters( + index = "1", + arity = "1", + description = "Destination path: [file:][/]path/to/local/file or zk:/path/to/zk/node.") + private String dst; + + @picocli.CommandLine.Mixin RecursiveOption recursiveOpt; + + @picocli.CommandLine.Option( + names = {"--solr-home"}, + description = "Required to look up configuration for compressing state.json.", + paramLabel = "DIR") + private String solrHome; + + public ZkCpTool() { + this(new DefaultToolRuntime()); + } + public ZkCpTool(ToolRuntime runtime) { super(runtime); } @@ -119,11 +157,16 @@ public String getHeader() { @Override public void runImpl(CommandLine cli) throws Exception { String zkHost = CLIUtils.getZkHost(cli); - - echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); String src = cli.getArgs()[0]; String dst = cli.getArgs()[1]; boolean recursive = cli.hasOption(CommonCLIOptions.RECURSIVE_OPTION); + String solrHome = cli.getOptionValue(SOLR_HOME_OPTION); + doCp(zkHost, src, dst, recursive, solrHome); + } + + private void doCp(String zkHost, String src, String dst, boolean recursive, String solrHome) + throws Exception { + echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); echo("Copying from '" + src + "' to '" + dst + "'. ZooKeeper at " + zkHost); boolean srcIsZk = src.toLowerCase(Locale.ROOT).startsWith("zk:"); @@ -152,7 +195,6 @@ public void runImpl(CommandLine cli) throws Exception { Compressor compressor = new ZLibCompressor(); if (dstIsZk) { - String solrHome = cli.getOptionValue(SOLR_HOME_OPTION); if (StrUtils.isNullOrEmpty(solrHome)) { solrHome = System.getProperty("solr.home"); } @@ -210,6 +252,7 @@ public void runImpl(CommandLine cli) throws Exception { @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + doCp(zkOpts.resolveZkHost(), src, dst, recursiveOpt.recursive, solrHome); + return 0; } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java index cd09844e05c1..3f0b87db1627 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java @@ -18,33 +18,30 @@ import java.lang.invoke.MethodHandles; import java.util.concurrent.TimeUnit; -import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; import org.apache.solr.common.cloud.SolrZkClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import picocli.CommandLine; /** Supports zk ls command in the bin/solr script. */ -@picocli.CommandLine.Command( +@CommandLine.Command( name = "ls", mixinStandardHelpOptions = true, description = "List the contents of a ZooKeeper node.") public class ZkLsTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + @CommandLine.Mixin ZkConnectionOptions zkOpts; - @picocli.CommandLine.Parameters( + @CommandLine.Parameters( index = "0", arity = "1", description = "The path of the ZooKeeper znode path to list.") private String path; - @picocli.CommandLine.Option( - names = {"-r", "--recursive"}, - description = "Apply the command recursively.") - private boolean recursive; + @CommandLine.Mixin RecursiveOption recursiveOpt; public ZkLsTool() { this(new DefaultToolRuntime()); @@ -74,37 +71,30 @@ public String getUsage() { } @Override - public void runImpl(CommandLine cli) throws Exception { + public void runImpl(org.apache.commons.cli.CommandLine cli) throws Exception { String zkHost = CLIUtils.getZkHost(cli); String znode = cli.getArgs()[0]; + boolean recursive = cli.hasOption(CommonCLIOptions.RECURSIVE_OPTION); try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { - echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); - boolean recursive = cli.hasOption(CommonCLIOptions.RECURSIVE_OPTION); - echoIfVerbose( - "Getting listing for ZooKeeper node " - + znode - + " from ZooKeeper at " - + zkHost - + " recursive: " - + recursive); - runtime.print(zkClient.listZnode(znode, recursive)); + doLs(zkClient, zkHost, znode, recursive); } catch (Exception e) { log.error("Could not complete ls operation for reason: ", e); throw (e); } } - private void doLs(SolrZkClient zkClient) throws Exception { - echoIfVerbose("\nConnecting to ZooKeeper at " + zkOpts.zkHost + " ..."); + private void doLs(SolrZkClient zkClient, String zkHost, String znode, boolean recursive) + throws Exception { + echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); echoIfVerbose( "Getting listing for ZooKeeper node " - + path + + znode + " from ZooKeeper at " - + zkOpts.zkHost + + zkHost + " recursive: " + recursive); - runtime.print(zkClient.listZnode(path, recursive)); + runtime.print(zkClient.listZnode(znode, recursive)); } @Override @@ -116,7 +106,7 @@ public int callTool() throws Exception { .withUrl(zkHost) .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) .build()) { - doLs(zkClient); + doLs(zkClient, zkHost, path, recursiveOpt.recursive); return 0; } catch (Exception e) { log.error("Could not complete ls operation for reason: ", e); diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java index 5c0d51a30c19..62774298af05 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java @@ -19,14 +19,22 @@ import static org.apache.solr.packagemanager.PackageUtils.format; import java.lang.invoke.MethodHandles; +import java.util.concurrent.TimeUnit; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; +import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; import org.apache.solr.common.cloud.SolrZkClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Supports zk mkroot command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "mkroot", + mixinStandardHelpOptions = true, + description = + "Make a znode in ZooKeeper with no data. Can be used to make a path of arbitrary depth" + + " but primarily intended to create a 'chroot'.") public class ZkMkrootTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -37,6 +45,25 @@ public class ZkMkrootTool extends ToolBase { .desc("Raise an error if the root exists. Defaults to false.") .get(); + @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + + @picocli.CommandLine.Parameters( + index = "0", + arity = "1", + description = "The ZooKeeper znode path to create.") + private String path; + + @picocli.CommandLine.Option( + names = {"--fail-on-exists"}, + arity = "0..1", + fallbackValue = "true", + description = "Raise an error if the znode already exists. Defaults to false.") + private boolean failOnExists; + + public ZkMkrootTool() { + this(new DefaultToolRuntime()); + } + public ZkMkrootTool(ToolRuntime runtime) { super(runtime); } @@ -76,18 +103,34 @@ public void runImpl(CommandLine cli) throws Exception { boolean failOnExists = cli.hasOption(FAIL_ON_EXISTS_OPTION); try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { - echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); - - echo("Creating ZooKeeper path " + znode + " on ZooKeeper at " + zkHost); - zkClient.makePath(znode, failOnExists); + doMkroot(zkClient, zkHost, znode, failOnExists); } catch (Exception e) { log.error("Could not complete mkroot operation for reason: ", e); throw (e); } } + private void doMkroot(SolrZkClient zkClient, String zkHost, String znode, boolean failOnExists) + throws Exception { + echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); + echo("Creating ZooKeeper path " + znode + " on ZooKeeper at " + zkHost); + zkClient.makePath(znode, failOnExists); + } + @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + String zkHost = zkOpts.resolveZkHost(); + + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(zkHost) + .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + doMkroot(zkClient, zkHost, path, failOnExists); + return 0; + } catch (Exception e) { + log.error("Could not complete mkroot operation for reason: ", e); + throw (e); + } } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java index 759eacb1b7b9..33be052767e2 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java @@ -20,17 +20,47 @@ import java.lang.invoke.MethodHandles; import java.util.Locale; +import java.util.concurrent.TimeUnit; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; import org.apache.solr.common.cloud.SolrZkClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Supports zk mv command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "mv", + mixinStandardHelpOptions = true, + description = { + "Move (rename) a znode on ZooKeeper.", + "", + ", : ZooKeeper nodes, the 'zk:' prefix is optional.", + "If ends with '/', then will be a parent znode", + "and the last element of the path will be appended." + }) public class ZkMvTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + + @picocli.CommandLine.Parameters( + index = "0", + arity = "1", + description = "Source ZooKeeper znode path (zk: prefix optional).") + private String src; + + @picocli.CommandLine.Parameters( + index = "1", + arity = "1", + description = "Destination ZooKeeper znode path (zk: prefix optional).") + private String dst; + + public ZkMvTool() { + this(new DefaultToolRuntime()); + } + public ZkMvTool(ToolRuntime runtime) { super(runtime); } @@ -70,37 +100,52 @@ public String getHeader() { @Override public void runImpl(CommandLine cli) throws Exception { String zkHost = CLIUtils.getZkHost(cli); + String src = cli.getArgs()[0]; + String dst = cli.getArgs()[1]; try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { - echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); - String src = cli.getArgs()[0]; - String dst = cli.getArgs()[1]; - - if (src.toLowerCase(Locale.ROOT).startsWith("file:") - || dst.toLowerCase(Locale.ROOT).startsWith("file:")) { - throw new SolrServerException( - "mv command operates on znodes and 'file:' has been specified."); - } - String source = src; - if (src.toLowerCase(Locale.ROOT).startsWith("zk")) { - source = src.substring(3); - } - - String dest = dst; - if (dst.toLowerCase(Locale.ROOT).startsWith("zk")) { - dest = dst.substring(3); - } - - echo("Moving Znode " + source + " to " + dest + " on ZooKeeper at " + zkHost); - zkClient.moveZnode(source, dest); + doMv(zkClient, zkHost, src, dst); } catch (Exception e) { log.error("Could not complete mv operation for reason: ", e); throw (e); } } + private void doMv(SolrZkClient zkClient, String zkHost, String src, String dst) throws Exception { + echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); + if (src.toLowerCase(Locale.ROOT).startsWith("file:") + || dst.toLowerCase(Locale.ROOT).startsWith("file:")) { + throw new SolrServerException( + "mv command operates on znodes and 'file:' has been specified."); + } + String source = src; + if (src.toLowerCase(Locale.ROOT).startsWith("zk:")) { + source = src.substring(3); + } + + String dest = dst; + if (dst.toLowerCase(Locale.ROOT).startsWith("zk:")) { + dest = dst.substring(3); + } + + echo("Moving Znode " + source + " to " + dest + " on ZooKeeper at " + zkHost); + zkClient.moveZnode(source, dest); + } + @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + String zkHost = zkOpts.resolveZkHost(); + + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(zkHost) + .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + doMv(zkClient, zkHost, src, dst); + return 0; + } catch (Exception e) { + log.error("Could not complete mv operation for reason: ", e); + throw (e); + } } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java index 8aab732c6f8b..5d683e56eac1 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java @@ -18,17 +18,37 @@ import java.lang.invoke.MethodHandles; import java.util.Locale; +import java.util.concurrent.TimeUnit; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; import org.apache.solr.common.cloud.SolrZkClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Supports zk rm command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "rm", + mixinStandardHelpOptions = true, + description = "Remove a znode from ZooKeeper.") public class ZkRmTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + + @picocli.CommandLine.Parameters( + index = "0", + arity = "1", + description = "The ZooKeeper znode path to remove (zk: prefix optional).") + private String path; + + @picocli.CommandLine.Mixin RecursiveOption recursiveOpt; + + public ZkRmTool() { + this(new DefaultToolRuntime()); + } + public ZkRmTool(ToolRuntime runtime) { super(runtime); } @@ -54,10 +74,20 @@ public String getUsage() { @Override public void runImpl(CommandLine cli) throws Exception { String zkHost = CLIUtils.getZkHost(cli); - String target = cli.getArgs()[0]; boolean recursive = cli.hasOption(CommonCLIOptions.RECURSIVE_OPTION); + echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); + try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { + doRm(zkClient, zkHost, target, recursive); + } catch (Exception e) { + log.error("Could not complete rm operation for reason: ", e); + throw (e); + } + } + + private void doRm(SolrZkClient zkClient, String zkHost, String target, boolean recursive) + throws Exception { String znode = target; if (target.toLowerCase(Locale.ROOT).startsWith("zk:")) { znode = target.substring(3); @@ -65,28 +95,34 @@ public void runImpl(CommandLine cli) throws Exception { if (znode.equals("/")) { throw new SolrServerException("You may not remove the root ZK node ('/')!"); } - echoIfVerbose("\nConnecting to ZooKeeper at " + zkHost + " ..."); - try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { - if (!recursive && !zkClient.getChildren(znode, null).isEmpty()) { - throw new SolrServerException( - "ZooKeeper node " + znode + " has children and recursive has NOT been specified."); - } - echo( - "Removing ZooKeeper node " - + znode - + " from ZooKeeper at " - + zkHost - + " recursive: " - + recursive); - zkClient.clean(znode); - } catch (Exception e) { - log.error("Could not complete rm operation for reason: ", e); - throw (e); + if (!recursive && !zkClient.getChildren(znode, null).isEmpty()) { + throw new SolrServerException( + "ZooKeeper node " + znode + " has children and recursive has NOT been specified."); } + echo( + "Removing ZooKeeper node " + + znode + + " from ZooKeeper at " + + zkHost + + " recursive: " + + recursive); + zkClient.clean(znode); } @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + String zkHost = zkOpts.resolveZkHost(); + + try (SolrZkClient zkClient = + new SolrZkClient.Builder() + .withUrl(zkHost) + .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) + .build()) { + doRm(zkClient, zkHost, path, recursiveOpt.recursive); + return 0; + } catch (Exception e) { + log.error("Could not complete rm operation for reason: ", e); + throw (e); + } } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkTool.java b/solr/core/src/java/org/apache/solr/cli/ZkTool.java index af69c2461e25..1c744d43382f 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkTool.java @@ -27,7 +27,17 @@ name = "zk", mixinStandardHelpOptions = true, description = "Sub commands for working with ZooKeeper.", - subcommands = {ZkLsTool.class}) + footer = "\nPass --help or -h after any COMMAND to see command-specific usage information.", + subcommands = { + ConfigSetDownloadTool.class, + ConfigSetUploadTool.class, + ZkCpTool.class, + ZkLsTool.class, + ZkMkrootTool.class, + ZkMvTool.class, + ZkRmTool.class, + UpdateACLTool.class + }) public class ZkTool implements Callable { @CommandLine.Spec CommandLine.Model.CommandSpec spec; diff --git a/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsPicocliTest.java b/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsPicocliTest.java new file mode 100644 index 000000000000..39b593d7dc72 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsPicocliTest.java @@ -0,0 +1,50 @@ +/* + * 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.cli; + +import java.util.Arrays; + +/** + * Runs all {@link ZkSubcommandsTest} tests through the picocli invocation path. + * + *

All {@code @Test} methods are inherited from {@link ZkSubcommandsTest}. Only the tool + * invocation strategy is overridden here to use {@code picocli.CommandLine.execute()} instead of + * the commons-cli path. + */ +public class ZkSubcommandsPicocliTest extends ZkSubcommandsTest { + + @Override + protected int runTool(String[] args, Class clazz) throws Exception { + // args[0] is the tool/subcommand name used by commons-cli dispatch; strip it for picocli. + String[] toolArgs = Arrays.copyOfRange(args, 1, args.length); + ToolBase tool = clazz.getDeclaredConstructor().newInstance(); + return new picocli.CommandLine(tool) + .setDefaultValueProvider(new CliDefaultValueProvider()) + .execute(toolArgs); + } + + @Override + protected int runTool( + String[] args, CLITestHelper.TestingRuntime runtime, Class clazz) + throws Exception { + String[] toolArgs = Arrays.copyOfRange(args, 1, args.length); + ToolBase tool = clazz.getDeclaredConstructor(ToolRuntime.class).newInstance(runtime); + return new picocli.CommandLine(tool) + .setDefaultValueProvider(new CliDefaultValueProvider()) + .execute(toolArgs); + } +} diff --git a/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsTest.java b/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsTest.java index d5aa1a72d5ae..4aebf94ee765 100644 --- a/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsTest.java +++ b/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsTest.java @@ -46,6 +46,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Tests ZK subcommands by invoking tool instances directly (not through the bin/solr script). + * + *

Subclasses override {@link #runTool} to exercise different invocation paths (commons-cli vs + * picocli). All {@code @Test} methods are inherited and run in each subclass. + * + * @see ZkSubcommandsPicocliTest + */ public class ZkSubcommandsTest extends SolrTestCaseJ4 { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -89,6 +97,29 @@ public void setUp() throws Exception { } } + /** + * Runs a tool via the invocation path under test. {@code args[0]} is the tool name. + * + *

The default implementation uses commons-cli via {@link CLITestHelper#runTool}. Subclasses + * override this to exercise a different invocation path (e.g. picocli). + */ + protected int runTool(String[] args, Class clazz) throws Exception { + return CLITestHelper.runTool(args, clazz); + } + + /** + * Runs a tool with output capture via the invocation path under test. {@code args[0]} is the tool + * name. + * + *

The default implementation uses commons-cli via {@link CLITestHelper#runTool}. Subclasses + * override this to exercise a different invocation path (e.g. picocli). + */ + protected int runTool( + String[] args, CLITestHelper.TestingRuntime runtime, Class clazz) + throws Exception { + return CLITestHelper.runTool(args, runtime, clazz); + } + @Test public void testPut() throws Exception { // test put @@ -101,7 +132,7 @@ public void testPut() throws Exception { String[] args = new String[] {"cp", "-z", zkServer.getZkAddress(), localFile.toString(), "zk:/data.txt"}; - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); assertArrayEquals( zkClient.getData("/data.txt", null, null), data.getBytes(StandardCharsets.UTF_8)); @@ -114,7 +145,7 @@ public void testPut() throws Exception { writer.write(data); writer.close(); - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); assertArrayEquals( zkClient.getData("/data.txt", null, null), data.getBytes(StandardCharsets.UTF_8)); @@ -139,7 +170,7 @@ public void testPutCompressed() throws Exception { String[] args = new String[] {"cp", "-z", zkServer.getZkAddress(), localFile.toString(), "zk:/state.json"}; - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); assertArrayEquals(dataBytes, zkClient.getCuratorFramework().getData().forPath("/state.json")); assertArrayEquals( @@ -160,7 +191,7 @@ public void testPutCompressed() throws Exception { args = new String[] {"cp", "-z", zkServer.getZkAddress(), localFile.toString(), "zk:/state.json"}; - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); byte[] fromZkRaw = zkClient.getCuratorFramework().getData().undecompressed().forPath("/state.json"); @@ -193,7 +224,7 @@ public void testPutFile() throws Exception { "zk:/foo.xml" }; - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); String fromZk = new String(zkClient.getData("/foo.xml", null, null), StandardCharsets.UTF_8); Path localFile = SOLR_HOME.resolve("solr-stress-new.xml"); @@ -213,7 +244,7 @@ public void testPutFileWithoutSlash() throws Exception { "zk:foo.xml" }; - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); String fromZk = new String(zkClient.getData("/foo.xml", null, null), StandardCharsets.UTF_8); Path localFile = SOLR_HOME.resolve("solr-stress-new.xml"); @@ -235,7 +266,7 @@ public void testPutFileCompressed() throws Exception { "zk:/state.json" }; - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); Path locFile = SOLR_HOME.resolve("solr-stress-new.xml"); byte[] fileBytes = Files.readAllBytes(locFile); @@ -252,7 +283,7 @@ public void testPutFileCompressed() throws Exception { "Should get back an uncompressed version what we put in ZK", fileBytes, fromZk); // Let's do it again - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); locFile = SOLR_HOME.resolve("solr-stress-new.xml"); fileBytes = Files.readAllBytes(locFile); @@ -279,7 +310,7 @@ public void testPutFileNotExists() throws Exception { "zk:/foo.xml" }; - assertEquals(1, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(1, runTool(args, ZkCpTool.class)); } @Test @@ -287,10 +318,10 @@ public void testLs() throws Exception { zkClient.makePath("/test/path", true); // test what happens when path arg "/" isn't the last one. - String[] args = new String[] {"ls", "/", "-r", "true", "-z", zkServer.getZkAddress()}; + String[] args = new String[] {"ls", "/", "-r", "-z", zkServer.getZkAddress()}; CLITestHelper.TestingRuntime runtime = new CLITestHelper.TestingRuntime(true); - assertEquals(0, CLITestHelper.runTool(args, runtime, ZkLsTool.class)); + assertEquals(0, runTool(args, runtime, ZkLsTool.class)); final String standardOutput2 = runtime.getOutput(); @@ -318,7 +349,7 @@ public void testUpConfigDownConfigClearZk() throws Exception { zkServer.getZkAddress() }; - assertEquals(0, CLITestHelper.runTool(args, ConfigSetUploadTool.class)); + assertEquals(0, runTool(args, ConfigSetUploadTool.class)); assertTrue(zkClient.exists(ZkConfigSetService.CONFIGS_ZKNODE + "/" + confsetname)); final Path confDir = ExternalPaths.TECHPRODUCTS_CONFIGSET; @@ -349,7 +380,7 @@ public void testUpConfigDownConfigClearZk() throws Exception { zkServer.getZkAddress() }; - assertEquals(0, CLITestHelper.runTool(args, ConfigSetDownloadTool.class)); + assertEquals(0, runTool(args, ConfigSetDownloadTool.class)); try (Stream filesStream = Files.list(destDir.resolve("conf"))) { List files = filesStream.toList(); @@ -395,7 +426,7 @@ public void testUpConfigDownConfigClearZk() throws Exception { // test reset zk args = new String[] {"rm", "-r", "-z", zkServer.getZkAddress(), "zk:/configs/confsetone"}; - assertEquals(0, CLITestHelper.runTool(args, ZkRmTool.class)); + assertEquals(0, runTool(args, ZkRmTool.class)); assertEquals(0, zkClient.getChildren("/configs", null).size()); } @@ -412,7 +443,7 @@ public void testGet() throws Exception { new String[] {"cp", "-z", zkServer.getZkAddress(), "zk:" + getNode, localFile.toString()}; CLITestHelper.TestingRuntime runtime = new CLITestHelper.TestingRuntime(true); - assertEquals(0, CLITestHelper.runTool(args, runtime, ZkCpTool.class)); + assertEquals(0, runTool(args, runtime, ZkCpTool.class)); final String standardOutput2 = runtime.getOutput(); assertTrue(standardOutput2.startsWith("Copying from 'zk:/getNode'")); @@ -438,7 +469,7 @@ public void testGetCompressed() throws Exception { String[] args = new String[] {"cp", "-z", zkServer.getZkAddress(), "zk:" + getNode, localFile.toString()}; - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); assertArrayEquals(data, Files.readAllBytes(localFile)); } @@ -457,7 +488,7 @@ public void testGetFile() throws Exception { // Not setting --zk-host, will fall back to sysProp 'zkHost' String[] args = new String[] {"cp", "zk:" + getNode, file.toString()}; - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); assertArrayEquals(data, Files.readAllBytes(file)); } @@ -480,7 +511,7 @@ public void testGetFileCompressed() throws Exception { String[] args = new String[] {"cp", "-z", zkServer.getZkAddress(), "zk:" + getNode, file.toString()}; - assertEquals(0, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(0, runTool(args, ZkCpTool.class)); assertArrayEquals(data, Files.readAllBytes(file)); } @@ -494,19 +525,72 @@ public void testGetFileNotExists() throws Exception { String[] args = new String[] {"cp", "-z", zkServer.getZkAddress(), "zk:" + getNode, file.toString()}; - assertEquals(1, CLITestHelper.runTool(args, ZkCpTool.class)); + assertEquals(1, runTool(args, ZkCpTool.class)); } @Test public void testInvalidZKAddress() throws Exception { - String[] args = new String[] {"ls", "/", "-r", "-z", "----------:33332"}; + String[] args = new String[] {"ls", "-r", "-z", "----------:33332", "/"}; + + assertEquals(1, runTool(args, ZkLsTool.class)); + } + + @Test + public void testMkroot() throws Exception { + String[] args = new String[] {"mkroot", "-z", zkServer.getZkAddress(), "/mkroot-test/a/b/c"}; + + assertEquals(0, runTool(args, ZkMkrootTool.class)); + assertTrue("Created path should exist", zkClient.exists("/mkroot-test/a/b/c")); - assertEquals(1, CLITestHelper.runTool(args, ZkLsTool.class)); + // --fail-on-exists with a value works for both commons-cli (hasArg) and picocli (arity 0..1) + args = + new String[] { + "mkroot", "--fail-on-exists", "true", "-z", zkServer.getZkAddress(), "/mkroot-test/a/b/c" + }; + assertEquals(1, runTool(args, ZkMkrootTool.class)); + + // Without --fail-on-exists, creating an existing path should succeed + args = new String[] {"mkroot", "-z", zkServer.getZkAddress(), "/mkroot-test/a/b/c"}; + assertEquals(0, runTool(args, ZkMkrootTool.class)); + } + + @Test + public void testMv() throws Exception { + zkClient.makePath("/mv-src", true); + + String[] args = new String[] {"mv", "-z", zkServer.getZkAddress(), "/mv-src", "/mv-dst"}; + assertEquals(0, runTool(args, ZkMvTool.class)); + assertFalse("Source should be gone after mv", zkClient.exists("/mv-src")); + assertTrue("Destination should exist after mv", zkClient.exists("/mv-dst")); + + // file: prefix should cause failure + args = new String[] {"mv", "-z", zkServer.getZkAddress(), "file:/mv-dst", "/mv-dst2"}; + assertEquals(1, runTool(args, ZkMvTool.class)); + } + + @Test + public void testRm() throws Exception { + zkClient.makePath("/rm-test/child", true); + + // Should fail without --recursive when node has children + String[] args = new String[] {"rm", "-z", zkServer.getZkAddress(), "/rm-test"}; + assertEquals(1, runTool(args, ZkRmTool.class)); + assertTrue("Node should still exist after failed rm", zkClient.exists("/rm-test")); + + // Should succeed with --recursive + args = new String[] {"rm", "-r", "-z", zkServer.getZkAddress(), "/rm-test"}; + assertEquals(0, runTool(args, ZkRmTool.class)); + assertFalse("Node should be gone after rm -r", zkClient.exists("/rm-test")); + + // Removing root should fail + args = new String[] {"rm", "-r", "-z", zkServer.getZkAddress(), "zk:/"}; + assertEquals(1, runTool(args, ZkRmTool.class)); } @Test public void testSetClusterProperty() throws Exception { + // ClusterTool is not a picocli ZK subcommand; always use the commons-cli path. ClusterProperties properties = new ClusterProperties(zkClient); // add property urlScheme=http String[] args = @@ -539,7 +623,7 @@ public void testUpdateAcls() throws Exception { VMParamsZkCredentialsInjector.DEFAULT_DIGEST_READONLY_PASSWORD_VM_PARAM_NAME, "pass"); String[] args = new String[] {"updateacls", "/", "-z", zkServer.getZkAddress()}; - assertEquals(0, CLITestHelper.runTool(args, UpdateACLTool.class)); + assertEquals(0, runTool(args, UpdateACLTool.class)); boolean excepted = false; try (SolrZkClient zkClient = diff --git a/solr/packaging/build.gradle b/solr/packaging/build.gradle index 7320f2ebe2b2..fa8fef586022 100644 --- a/solr/packaging/build.gradle +++ b/solr/packaging/build.gradle @@ -309,6 +309,8 @@ task integrationTests(type: BatsTask) { environment TEST_OUTPUT_DIR: integrationTestOutput environment TEST_FAILURE_DIR: solrTestFailuresDir environment BATS_LIB_PREFIX: "$nodeProjectDir/node_modules" + // TODO: SOLR-17697 picocli, run all BATS tests using picocli. + //environment SOLR_PICOCLI: 'true' } class BatsTask extends Exec { diff --git a/solr/packaging/test/test_zk.bats b/solr/packaging/test/test_zk.bats index 11ee3a7a76d1..66178030c117 100644 --- a/solr/packaging/test/test_zk.bats +++ b/solr/packaging/test/test_zk.bats @@ -48,15 +48,18 @@ teardown() { @test "long help" { run solr zk -h - assert_output --partial "bin/solr zk ls" - assert_output --partial "bin/solr zk updateacls" + # TODO: Long help is different in picocli + #assert_output --partial "bin/solr zk ls" + #assert_output --partial "bin/solr zk updateacls" assert_output --partial "Pass --help or -h after any COMMAND" + assert_output --partial "updateacls" } -@test "running subcommands with zk is prevented" { - run solr ls / -z localhost:${ZK_PORT} - assert_output --partial "You must invoke this subcommand using the zk command" -} +# TODO: Picocli fails in a different way when a subcommand is not invoked using the zk command +#@test "running subcommands with zk is prevented" { +# run solr ls / -z localhost:${ZK_PORT} +# assert_output --partial "You must invoke this subcommand using the zk command" +#} @test "listing out files" { sleep 1 From 5cd2ab398cf7b78b16dde08c5478c1ee7ee5ad55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 9 Apr 2026 12:02:24 +0200 Subject: [PATCH 07/19] SOLR-17697 Remove --version option for all tools except top-level bin/solr (#4274) --- .../solr/cli/ConfigSetDownloadTool.java | 1 - .../apache/solr/cli/ConfigSetUploadTool.java | 1 - .../java/org/apache/solr/cli/HelpMixin.java | 32 +++++++++++++++++++ .../src/java/org/apache/solr/cli/SolrCLI.java | 4 --- .../org/apache/solr/cli/StartCommand.java | 7 ++-- .../java/org/apache/solr/cli/StatusTool.java | 5 +-- .../java/org/apache/solr/cli/StopCommand.java | 4 ++- .../java/org/apache/solr/cli/ToolBase.java | 2 ++ .../org/apache/solr/cli/UpdateACLTool.java | 1 - .../java/org/apache/solr/cli/ZkCpTool.java | 1 - .../java/org/apache/solr/cli/ZkLsTool.java | 5 +-- .../org/apache/solr/cli/ZkMkrootTool.java | 1 - .../java/org/apache/solr/cli/ZkMvTool.java | 1 - .../java/org/apache/solr/cli/ZkRmTool.java | 5 +-- .../src/java/org/apache/solr/cli/ZkTool.java | 3 +- 15 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/cli/HelpMixin.java diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java index aa5b3c91fac6..0d812de23fb9 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java @@ -31,7 +31,6 @@ /** Supports zk downconfig command in the bin/solr script. */ @picocli.CommandLine.Command( name = "downconfig", - mixinStandardHelpOptions = true, description = "Download a configset from ZooKeeper to the local filesystem.") public class ConfigSetDownloadTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java index 87bfd2fb87a4..156681b48891 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java @@ -33,7 +33,6 @@ /** Supports zk upconfig command in the bin/solr script. */ @picocli.CommandLine.Command( name = "upconfig", - mixinStandardHelpOptions = true, description = "Upload a configset from the local filesystem to ZooKeeper.") public class ConfigSetUploadTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/HelpMixin.java b/solr/core/src/java/org/apache/solr/cli/HelpMixin.java new file mode 100644 index 000000000000..57e2caae50a6 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/HelpMixin.java @@ -0,0 +1,32 @@ +/* + * 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.cli; + +import picocli.CommandLine; + +/** + * Picocli mixin that adds a {@code --help} option to a command without also adding {@code + * --version}. Prefer this over {@code mixinStandardHelpOptions = true} on subcommands, since {@code + * --version} is only meaningful on the top-level {@code bin/solr} command. + */ +public class HelpMixin { + @CommandLine.Option( + names = {"-h", "--help"}, + usageHelp = true, + description = "Print this help message and exit.") + public boolean help; +} diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index 12e1ce518cb8..9f62a90868c3 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -60,7 +60,6 @@ mixinStandardHelpOptions = true, synopsisHeading = "usage: ", commandListHeading = "\nCommands:\n", - descriptionHeading = "Global options:\n", footer = { "", "SolrCloud example (embedded Zookeeper):", @@ -111,9 +110,6 @@ public static void main(String[] args) throws Exception { private static void propagateCommandSettings(picocli.CommandLine cmd) { for (picocli.CommandLine subcommand : cmd.getSubcommands().values()) { subcommand.getCommandSpec().defaultValueProvider(cmd.getCommandSpec().defaultValueProvider()); - subcommand - .getCommandSpec() - .mixinStandardHelpOptions(cmd.getCommandSpec().mixinStandardHelpOptions()); subcommand.getCommandSpec().usageMessage().width(cmd.getCommandSpec().usageMessage().width()); subcommand .getCommandSpec() diff --git a/solr/core/src/java/org/apache/solr/cli/StartCommand.java b/solr/core/src/java/org/apache/solr/cli/StartCommand.java index b7c0b3f07c5b..ed173a42beef 100644 --- a/solr/core/src/java/org/apache/solr/cli/StartCommand.java +++ b/solr/core/src/java/org/apache/solr/cli/StartCommand.java @@ -19,12 +19,11 @@ import java.util.concurrent.Callable; import picocli.CommandLine; -@CommandLine.Command( - name = "start", - description = "Starts Solr in standalone or SolrCloud mode.", - mixinStandardHelpOptions = true) +@CommandLine.Command(name = "start", description = "Starts Solr in standalone or SolrCloud mode.") public class StartCommand implements Callable { + @CommandLine.Mixin HelpMixin helpMixin; + @CommandLine.Spec CommandLine.Model.CommandSpec spec; @CommandLine.Option( diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index bbe253a6887f..081bd57bca41 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -44,10 +44,7 @@ * *

Get the status of a Solr server. */ -@picocli.CommandLine.Command( - name = "status", - mixinStandardHelpOptions = true, - description = "Get the status of a Solr server.") +@picocli.CommandLine.Command(name = "status", description = "Get the status of a Solr server.") public class StatusTool extends ToolBase { @picocli.CommandLine.Option( names = {"--max-wait-secs"}, diff --git a/solr/core/src/java/org/apache/solr/cli/StopCommand.java b/solr/core/src/java/org/apache/solr/cli/StopCommand.java index f9f3fc626280..0803cea767ac 100644 --- a/solr/core/src/java/org/apache/solr/cli/StopCommand.java +++ b/solr/core/src/java/org/apache/solr/cli/StopCommand.java @@ -22,9 +22,11 @@ * This class is currently only used for printing CLI usage. The stop logic is currently handled in * start script. */ -@CommandLine.Command(name = "stop", description = "Stops Solr.", mixinStandardHelpOptions = true) +@CommandLine.Command(name = "stop", description = "Stops Solr.") public class StopCommand { + @CommandLine.Mixin HelpMixin helpMixin; + @CommandLine.Option( names = {"-p", "--port"}, description = diff --git a/solr/core/src/java/org/apache/solr/cli/ToolBase.java b/solr/core/src/java/org/apache/solr/cli/ToolBase.java index 8b3d4304cf3c..c05595c94f62 100644 --- a/solr/core/src/java/org/apache/solr/cli/ToolBase.java +++ b/solr/core/src/java/org/apache/solr/cli/ToolBase.java @@ -26,6 +26,8 @@ import org.apache.solr.util.StartupLoggingUtils; public abstract class ToolBase implements Tool, Callable { + @picocli.CommandLine.Mixin HelpMixin helpMixin; + @picocli.CommandLine.Option( names = {"-v", "--verbose"}, description = "Enable verbose mode.") diff --git a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java index cb79d0bf42c2..c0ebbe449022 100644 --- a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java +++ b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java @@ -31,7 +31,6 @@ */ @picocli.CommandLine.Command( name = "updateacls", - mixinStandardHelpOptions = true, description = "Update ACLs for a ZooKeeper znode.") public class UpdateACLTool extends ToolBase { // It is a shame this tool doesn't more closely mimic how the ConfigTool works. diff --git a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java index e28a57715f6d..292f88c39f41 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java @@ -41,7 +41,6 @@ /** Supports zk cp command in the bin/solr script. */ @picocli.CommandLine.Command( name = "cp", - mixinStandardHelpOptions = true, description = { "Copy files or folders to/from ZooKeeper or ZooKeeper to ZooKeeper.", "", diff --git a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java index 3f0b87db1627..56e4ef21b7de 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java @@ -26,10 +26,7 @@ import picocli.CommandLine; /** Supports zk ls command in the bin/solr script. */ -@CommandLine.Command( - name = "ls", - mixinStandardHelpOptions = true, - description = "List the contents of a ZooKeeper node.") +@CommandLine.Command(name = "ls", description = "List the contents of a ZooKeeper node.") public class ZkLsTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java index 62774298af05..82573f74dc58 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java @@ -31,7 +31,6 @@ /** Supports zk mkroot command in the bin/solr script. */ @picocli.CommandLine.Command( name = "mkroot", - mixinStandardHelpOptions = true, description = "Make a znode in ZooKeeper with no data. Can be used to make a path of arbitrary depth" + " but primarily intended to create a 'chroot'.") diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java index 33be052767e2..9036e461bfc8 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java @@ -32,7 +32,6 @@ /** Supports zk mv command in the bin/solr script. */ @picocli.CommandLine.Command( name = "mv", - mixinStandardHelpOptions = true, description = { "Move (rename) a znode on ZooKeeper.", "", diff --git a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java index 5d683e56eac1..ed529066a108 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java @@ -28,10 +28,7 @@ import org.slf4j.LoggerFactory; /** Supports zk rm command in the bin/solr script. */ -@picocli.CommandLine.Command( - name = "rm", - mixinStandardHelpOptions = true, - description = "Remove a znode from ZooKeeper.") +@picocli.CommandLine.Command(name = "rm", description = "Remove a znode from ZooKeeper.") public class ZkRmTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/ZkTool.java b/solr/core/src/java/org/apache/solr/cli/ZkTool.java index 1c744d43382f..bed1805853ca 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkTool.java @@ -25,7 +25,6 @@ */ @CommandLine.Command( name = "zk", - mixinStandardHelpOptions = true, description = "Sub commands for working with ZooKeeper.", footer = "\nPass --help or -h after any COMMAND to see command-specific usage information.", subcommands = { @@ -40,6 +39,8 @@ }) public class ZkTool implements Callable { + @CommandLine.Mixin HelpMixin helpMixin; + @CommandLine.Spec CommandLine.Model.CommandSpec spec; @Override From 41292cec84b59269bcf29e30ee05a263e26646b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 14 May 2026 20:39:29 +0200 Subject: [PATCH 08/19] SOLR-17697 Implement picocli for create and delete commands (#4269) --- .../apache/solr/cli/ConnectionOptions.java | 39 +++ .../java/org/apache/solr/cli/CreateTool.java | 223 +++++++++++++----- .../apache/solr/cli/CredentialsOptions.java | 30 +++ .../java/org/apache/solr/cli/DeleteTool.java | 146 +++++++++--- .../src/java/org/apache/solr/cli/SolrCLI.java | 4 +- .../java/org/apache/solr/cli/ToolBase.java | 10 +- solr/packaging/test/test_create.bats | 2 +- 7 files changed, 353 insertions(+), 101 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java create mode 100644 solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java diff --git a/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java new file mode 100644 index 000000000000..b34d81f4c6a2 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java @@ -0,0 +1,39 @@ +/* + * 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.cli; + +/** + * Picocli ArgGroup for mutually-exclusive Solr URL / ZooKeeper connection options. + * + *

Use as the type of an {@code @ArgGroup(exclusive = true, multiplicity = "0..1")} field to + * ensure the user provides at most one of {@code --solr-url} or {@code --zk-host}. + */ +class ConnectionOptions { + @picocli.CommandLine.Option( + names = {"-s", "--solr-url"}, + description = + "Base Solr URL, which can be used to determine the zk-host if that's not known.") + String solrUrl; + + @picocli.CommandLine.Option( + names = {"-z", "--zk-host"}, + description = + "Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to " + + CommonCLIOptions.DefaultValues.ZK_HOST + + ".") + String zkHost; +} diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java b/solr/core/src/java/org/apache/solr/cli/CreateTool.java index 1f81609f0b78..ed1995d94345 100644 --- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java @@ -21,6 +21,7 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.commons.cli.CommandLine; @@ -38,9 +39,15 @@ import org.apache.solr.client.solrj.response.SystemInfoResponse; import org.apache.solr.cloud.ZkConfigSetService; import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.util.EnvUtils; import org.apache.solr.core.ConfigSetService; /** Supports create command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "create", + mixinStandardHelpOptions = true, + description = + "Creates a core or collection depending on whether Solr is running in standalone (core) or SolrCloud mode (collection).") public class CreateTool extends ToolBase { private static final Option COLLECTION_NAME_OPTION = @@ -90,6 +97,60 @@ public class CreateTool extends ToolBase { .desc("Configuration name; default is the collection name.") .get(); + /** Options bean shared between commons-cli and picocli paths. */ + record CreateParams( + String name, + String confDir, + String confName, + String solrUrl, + String credentials, + int shards, + int replicationFactor) {} + + // --- picocli fields --- + + @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1") + private ConnectionOptions connectionOptions; + + @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions; + + @picocli.CommandLine.Option( + names = {"-c", "--name"}, + required = true, + description = "Name of collection or core to create.") + private String name; + + @picocli.CommandLine.Option( + names = {"-sh", "--shards"}, + description = "Number of shards; default is 1.", + defaultValue = "1") + private int shards; + + @picocli.CommandLine.Option( + names = {"-rf", "--replication-factor"}, + description = + "Number of copies of each document across the collection (replicas per shard); default is 1.", + defaultValue = "1") + private int replicationFactor; + + @picocli.CommandLine.Option( + names = {"-d", "--conf-dir"}, + description = + "Configuration directory to copy when creating the new collection; default is " + + DefaultValues.DEFAULT_CONFIG_SET + + ".", + defaultValue = DefaultValues.DEFAULT_CONFIG_SET) + private String confDir; + + @picocli.CommandLine.Option( + names = {"-n", "--conf-name"}, + description = "Configuration name; default is the collection name.") + private String confName; + + public CreateTool() { + this(new DefaultToolRuntime()); + } + public CreateTool(ToolRuntime runtime) { super(runtime); } @@ -123,45 +184,45 @@ public Options getOptions() { @Override public void runImpl(CommandLine cli) throws Exception { try (var solrClient = CLIUtils.getSolrClient(cli)) { + CreateParams params = + new CreateParams( + cli.getOptionValue(COLLECTION_NAME_OPTION), + cli.getOptionValue(CONF_DIR_OPTION, DefaultValues.DEFAULT_CONFIG_SET), + cli.getOptionValue(CONF_NAME_OPTION), + cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, CLIUtils.getDefaultSolrUrl()), + cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), + cli.getParsedOptionValue(SHARDS_OPTION, 1), + cli.getParsedOptionValue(REPLICATION_FACTOR_OPTION, 1)); if (CLIUtils.isCloudMode(solrClient)) { - createCollection(cli); + createCollection(CLIUtils.getZkHost(cli), params); } else { - createCore(cli, solrClient); + createCore(params, solrClient); } } } - protected void createCore(CommandLine cli, SolrClient solrClient) throws Exception { - String coreName = cli.getOptionValue(COLLECTION_NAME_OPTION); - String solrUrl = - cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, CLIUtils.getDefaultSolrUrl()); - - final String solrInstallDir = System.getProperty("solr.install.dir"); - final String confDirName = - cli.getOptionValue(CONF_DIR_OPTION, DefaultValues.DEFAULT_CONFIG_SET); - + private void createCore(CreateParams params, SolrClient solrClient) throws Exception { // we allow them to pass a directory instead of a configset name - Path configsetDir = Path.of(confDirName); - Path solrInstallDirPath = Path.of(solrInstallDir); + Path configsetDir = Path.of(params.confDir); + Path solrInstallDirPath = Path.of(System.getProperty("solr.install.dir")); if (!Files.isDirectory(configsetDir)) { ensureConfDirExists(solrInstallDirPath, configsetDir); } - printDefaultConfigsetWarningIfNecessary(cli); + printDefaultConfigsetWarning(params); SystemInfoResponse sysResponse = (new SystemInfoRequest()).process(solrClient); // usually same as solr home, but not always String coreRootDirectory = sysResponse.getCoreRoot(); - if (CLIUtils.safeCheckCoreExists( - solrUrl, coreName, cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) { + if (CLIUtils.safeCheckCoreExists(params.solrUrl, params.name, params.credentials)) { throw new IllegalArgumentException( "\nCore '" - + coreName + + params.name + "' already exists!\nChecked core existence using Core API command"); } - Path coreInstanceDir = Path.of(coreRootDirectory, coreName); + Path coreInstanceDir = Path.of(coreRootDirectory, params.name); Path confDir = getFullConfDir(solrInstallDirPath, configsetDir).resolve("conf"); if (!Files.isDirectory(coreInstanceDir)) { Files.createDirectories(coreInstanceDir); @@ -177,14 +238,14 @@ protected void createCore(CommandLine cli, SolrClient solrClient) throws Excepti + coreInstanceDir.toAbsolutePath()); } - echoIfVerbose("\nCreating new core '" + coreName + "' using V2 Cores API"); + echoIfVerbose("\nCreating new core '" + params.name + "' using V2 Cores API"); try { var req = new CoresApi.CreateCore(); - req.setName(coreName); - req.setInstanceDir(coreName); + req.setName(params.name); + req.setInstanceDir(params.name); req.process(solrClient); - echo(String.format(Locale.ROOT, "\nCreated new core '%s'", coreName)); + echo(String.format(Locale.ROOT, "\nCreated new core '%s'", params.name)); } catch (Exception e) { /* create-core failed, cleanup the copied configset before propagating the error. */ @@ -193,32 +254,26 @@ protected void createCore(CommandLine cli, SolrClient solrClient) throws Excepti } } - protected void createCollection(CommandLine cli) throws Exception { + private void createCollection(String zkHost, CreateParams params) throws Exception { var builder = new HttpJettySolrClient.Builder() .withIdleTimeout(30, TimeUnit.SECONDS) .withConnectionTimeout(15, TimeUnit.SECONDS) .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS) - .withOptionalBasicAuthCredentials( - cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION)); - String zkHost = CLIUtils.getZkHost(cli); + .withOptionalBasicAuthCredentials(params.credentials); echoIfVerbose("Connecting to ZooKeeper at " + zkHost); try (CloudSolrClient cloudSolrClient = CLIUtils.getCloudSolrClient(zkHost, builder)) { - createCollection(cloudSolrClient, cli); + createCollection(cloudSolrClient, params); } } - protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli) + private void createCollection(CloudSolrClient cloudSolrClient, CreateParams params) throws Exception { - String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION); - final String solrInstallDir = System.getProperty("solr.install.dir"); - String confName = cli.getOptionValue(CONF_NAME_OPTION); - String confDir = cli.getOptionValue(CONF_DIR_OPTION, DefaultValues.DEFAULT_CONFIG_SET); - Path solrInstallDirPath = Path.of(solrInstallDir); - Path confDirPath = Path.of(confDir); + Path solrInstallDirPath = Path.of(System.getProperty("solr.install.dir")); + Path confDirPath = Path.of(params.confDir); ensureConfDirExists(solrInstallDirPath, confDirPath); - printDefaultConfigsetWarningIfNecessary(cli); + printDefaultConfigsetWarning(params); Set liveNodes = cloudSolrClient.getClusterState().getLiveNodes(); if (liveNodes.isEmpty()) @@ -226,15 +281,16 @@ protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli "No live nodes found! Cannot create a collection until " + "there is at least 1 live node in the cluster."); - String solrUrl = cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION); + String solrUrl = params.solrUrl; if (solrUrl == null) { String firstLiveNode = liveNodes.iterator().next(); solrUrl = ZkStateReader.from(cloudSolrClient).getBaseUrlForNodeName(firstLiveNode); } // build a URL to create the collection - int numShards = cli.getParsedOptionValue(SHARDS_OPTION, 1); - int replicationFactor = cli.getParsedOptionValue(REPLICATION_FACTOR_OPTION, 1); + int numShards = params.shards; + int replicationFactor = params.replicationFactor; + String confName = params.confName; boolean configExistsInZk = confName != null @@ -245,14 +301,15 @@ protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli echo("Re-using existing configuration directory " + confName); } else { // if (confdir != null && !confdir.trim().isEmpty()) { if (confName == null || confName.trim().isEmpty()) { - confName = collectionName; + confName = params.name; } // TODO: This should be done using the configSet API final Path configsetsDirPath = CLIUtils.getConfigSetsDir(solrInstallDirPath); ConfigSetService configSetService = new ZkConfigSetService(ZkStateReader.from(cloudSolrClient).getZkClient()); - Path confPath = ConfigSetService.getConfigsetPath(confDir, configsetsDirPath.toString()); + Path confPath = + ConfigSetService.getConfigsetPath(params.confDir, configsetsDirPath.toString()); echoIfVerbose( "Uploading " @@ -266,20 +323,19 @@ protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli } // since creating a collection is a heavy-weight operation, check for existence first - if (CLIUtils.safeCheckCollectionExists( - solrUrl, collectionName, cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION))) { + if (CLIUtils.safeCheckCollectionExists(solrUrl, params.name, params.credentials)) { throw new IllegalStateException( "\nCollection '" - + collectionName + + params.name + "' already exists!\nChecked collection existence using V2 Collections API"); } // doesn't seem to exist ... try to create - echoIfVerbose("\nCreating new collection '" + collectionName + "' using V2 Collections API"); + echoIfVerbose("\nCreating new collection '" + params.name + "' using V2 Collections API"); try { var req = new CollectionsApi.CreateCollection(); - req.setName(collectionName); + req.setName(params.name); req.setConfig(confName); req.setNumShards(numShards); req.setReplicationFactor(replicationFactor); @@ -287,14 +343,14 @@ protected void createCollection(CloudSolrClient cloudSolrClient, CommandLine cli echoIfVerbose(response); } catch (SolrServerException sse) { throw new Exception( - "Failed to create collection '" + collectionName + "' due to: " + sse.getMessage()); + "Failed to create collection '" + params.name + "' due to: " + sse.getMessage()); } String endMessage = String.format( Locale.ROOT, "Created collection '%s' with %d shard(s), %d replica(s)", - collectionName, + params.name, numShards, replicationFactor); if (confName != null && !confName.trim().isEmpty()) { @@ -319,16 +375,18 @@ private void ensureConfDirExists(Path solrInstallDir, Path confDirName) { } } - private void printDefaultConfigsetWarningIfNecessary(CommandLine cli) { - final String confDirectoryName = - cli.getOptionValue(CONF_DIR_OPTION, DefaultValues.DEFAULT_CONFIG_SET); - final String confName = cli.getOptionValue(CONF_NAME_OPTION, ""); + private void printDefaultConfigsetWarning(CreateParams params) { + printDefaultConfigsetWarning( + params.confDir, + params.confName != null ? params.confName : "", + params.name, + params.solrUrl != null ? params.solrUrl : CLIUtils.getDefaultSolrUrl()); + } - if (confDirectoryName.equals("_default") - && (confName.isEmpty() || confName.equals("_default"))) { - final String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION); - final String solrUrl = - cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, CLIUtils.getDefaultSolrUrl()); + private void printDefaultConfigsetWarning( + String confDirName, String confNameArg, String collectionName, String solrUrl) { + if (confDirName.equals("_default") + && (confNameArg.isEmpty() || confNameArg.equals("_default"))) { final String curlCommand = String.format( Locale.ROOT, @@ -355,6 +413,57 @@ private void printDefaultConfigsetWarningIfNecessary(CommandLine cli) { @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + String zkHostArg = + (connectionOptions != null) ? connectionOptions.zkHost : EnvUtils.getProperty("zkHost"); + String solrUrlArg = (connectionOptions != null) ? connectionOptions.solrUrl : null; + + if (zkHostArg != null) { + CreateParams params = + new CreateParams( + name, + confDir, + confName, + null, + credentialsOptions.credentials, + shards, + replicationFactor); + createCollection(zkHostArg, params); + } else { + String resolvedSolrUrl; + if (solrUrlArg != null) { + resolvedSolrUrl = CLIUtils.normalizeSolrUrl(solrUrlArg); + } else { + resolvedSolrUrl = CLIUtils.getDefaultSolrUrl(); + CLIO.err( + "Neither --zk-host or --solr-url parameters, nor ZK_HOST env var provided, so assuming solr url is " + + resolvedSolrUrl + + "."); + } + CreateParams params = + new CreateParams( + name, + confDir, + confName, + resolvedSolrUrl, + credentialsOptions.credentials, + shards, + replicationFactor); + try (var solrClient = + CLIUtils.getSolrClient(resolvedSolrUrl, credentialsOptions.credentials)) { + Map status = StatusTool.reportStatus(solrClient); + @SuppressWarnings("unchecked") + Map cloud = (Map) status.get("cloud"); + if (cloud != null) { + String zookeeper = (String) cloud.get("ZooKeeper"); + if (zookeeper != null && zookeeper.endsWith("(embedded)")) { + zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length()); + } + createCollection(zookeeper, params); + } else { + createCore(params, solrClient); + } + } + } + return 0; } } diff --git a/solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java b/solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java new file mode 100644 index 000000000000..53bc24ca8b34 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java @@ -0,0 +1,30 @@ +/* + * 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.cli; + +/** + * Picocli mixin providing the common {@code --credentials} option. + * + *

Use as {@code @CommandLine.Mixin CredentialsOptions credentialsOptions} in a command class. + */ +public class CredentialsOptions { + @picocli.CommandLine.Option( + names = {"-u", "--credentials"}, + description = + "Credentials in the format username:password. Example: --credentials solr:SolrRocks") + public String credentials; +} diff --git a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java index 07f92894f311..25a49e4e82b3 100644 --- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java @@ -19,6 +19,7 @@ import java.lang.invoke.MethodHandles; import java.util.Collection; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -32,10 +33,16 @@ import org.apache.solr.client.solrj.request.CollectionsApi; import org.apache.solr.client.solrj.request.CoresApi; import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.util.EnvUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** Supports delete command in the bin/solr script. */ +@picocli.CommandLine.Command( + name = "delete", + mixinStandardHelpOptions = true, + description = + "Deletes a collection or core depending on whether Solr is running in SolrCloud or standalone mode.") public class DeleteTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -52,7 +59,7 @@ public class DeleteTool extends ToolBase { Option.builder() .longOpt("delete-config") .desc( - "Flag to indicate if the underlying configuration directory for a collection should also be deleted; default is true.") + "Flag to indicate if the underlying configuration directory for a collection should also be deleted; default is false.") .get(); private static final Option FORCE_OPTION = @@ -62,6 +69,38 @@ public class DeleteTool extends ToolBase { "Skip safety checks when deleting the configuration directory used by a collection.") .get(); + /** Options bean shared between commons-cli and picocli paths. */ + record DeleteParams(String name, String credentials, boolean deleteConfig, boolean force) {} + + // --- picocli fields --- + + @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1") + private ConnectionOptions connectionOptions; + + @picocli.CommandLine.Mixin private CredentialsOptions credentialsOptions; + + @picocli.CommandLine.Option( + names = {"-c", "--name"}, + required = true, + description = "Name of the core / collection to delete.") + private String name; + + @picocli.CommandLine.Option( + names = {"--delete-config"}, + description = + "Flag to indicate if the underlying configuration directory for a collection should also be deleted; default is false.") + private boolean deleteConfig; + + @picocli.CommandLine.Option( + names = {"-f", "--force"}, + description = + "Skip safety checks when deleting the configuration directory used by a collection.") + private boolean force; + + public DeleteTool() { + this(new DefaultToolRuntime()); + } + public DeleteTool(ToolRuntime runtime) { super(runtime); } @@ -93,31 +132,34 @@ public Options getOptions() { @Override public void runImpl(CommandLine cli) throws Exception { try (var solrClient = CLIUtils.getSolrClient(cli)) { + DeleteParams params = + new DeleteParams( + cli.getOptionValue(COLLECTION_NAME_OPTION), + cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), + cli.hasOption(DELETE_CONFIG_OPTION), + cli.hasOption(FORCE_OPTION)); if (CLIUtils.isCloudMode(solrClient)) { - deleteCollection(cli); + deleteCollection(CLIUtils.getZkHost(cli), params); } else { - deleteCore(cli, solrClient); + deleteCore(params, solrClient); } } } - protected void deleteCollection(CommandLine cli) throws Exception { + private void deleteCollection(String zkHost, DeleteParams params) throws Exception { var builder = new HttpJettySolrClient.Builder() .withIdleTimeout(30, TimeUnit.SECONDS) .withConnectionTimeout(15, TimeUnit.SECONDS) .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS) - .withOptionalBasicAuthCredentials( - cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION)); - - String zkHost = CLIUtils.getZkHost(cli); + .withOptionalBasicAuthCredentials(params.credentials); + echoIfVerbose("Connecting to ZooKeeper at " + zkHost); try (CloudSolrClient cloudSolrClient = CLIUtils.getCloudSolrClient(zkHost, builder)) { - echoIfVerbose("Connecting to ZooKeeper at " + zkHost); - deleteCollection(cloudSolrClient, cli); + deleteCollection(cloudSolrClient, params); } } - protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli) + private void deleteCollection(CloudSolrClient cloudSolrClient, DeleteParams params) throws Exception { Set liveNodes = cloudSolrClient.getClusterState().getLiveNodes(); if (liveNodes.isEmpty()) @@ -126,17 +168,15 @@ protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli + "there is at least 1 live node in the cluster."); ZkStateReader zkStateReader = ZkStateReader.from(cloudSolrClient); - String collectionName = cli.getOptionValue(COLLECTION_NAME_OPTION); - if (!zkStateReader.getClusterState().hasCollection(collectionName)) { - throw new IllegalArgumentException("Collection " + collectionName + " not found!"); + if (!zkStateReader.getClusterState().hasCollection(params.name)) { + throw new IllegalArgumentException("Collection " + params.name + " not found!"); } - String configName = - zkStateReader.getClusterState().getCollection(collectionName).getConfigName(); - boolean deleteConfig = cli.hasOption(DELETE_CONFIG_OPTION); + String configName = zkStateReader.getClusterState().getCollection(params.name).getConfigName(); + boolean effectiveDeleteConfig = params.deleteConfig; - if (deleteConfig && configName != null) { - if (cli.hasOption(FORCE_OPTION)) { + if (effectiveDeleteConfig && configName != null) { + if (params.force) { log.warn( "Skipping safety checks, configuration directory {} will be deleted with impunity.", configName); @@ -155,35 +195,35 @@ protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli Optional inUse = collections.stream() - .filter(name -> !name.equals(collectionName)) // ignore this collection + .filter(n -> !n.equals(params.name)) // ignore this collection .filter( - name -> + n -> configName.equals( - zkStateReader.getClusterState().getCollection(name).getConfigName())) + zkStateReader.getClusterState().getCollection(n).getConfigName())) .findFirst(); if (inUse.isPresent()) { - deleteConfig = false; + effectiveDeleteConfig = false; log.warn( "Configuration directory {} is also being used by {}{}", configName, inUse.get(), - "; configuration will not be deleted from ZooKeeper. You can pass the --force-delete-config flag to force delete."); + "; configuration will not be deleted from ZooKeeper. You can pass the --force flag to force delete."); } } } - echoIfVerbose("\nDeleting collection '" + collectionName + "' using V2 Collections API"); + echoIfVerbose("\nDeleting collection '" + params.name + "' using V2 Collections API"); try { - var req = new CollectionsApi.DeleteCollection(collectionName); + var req = new CollectionsApi.DeleteCollection(params.name); var response = req.process(cloudSolrClient); echoIfVerbose(response); } catch (SolrServerException sse) { throw new Exception( - "Failed to delete collection '" + collectionName + "' due to: " + sse.getMessage()); + "Failed to delete collection '" + params.name + "' due to: " + sse.getMessage()); } - if (deleteConfig) { + if (effectiveDeleteConfig) { String configZnode = "/configs/" + configName; try { zkStateReader.getZkClient().clean(configZnode); @@ -197,28 +237,62 @@ protected void deleteCollection(CloudSolrClient cloudSolrClient, CommandLine cli } } - echo(String.format(Locale.ROOT, "\nDeleted collection '%s'", collectionName)); + echo(String.format(Locale.ROOT, "\nDeleted collection '%s'", params.name)); } - protected void deleteCore(CommandLine cli, SolrClient solrClient) throws Exception { - String coreName = cli.getOptionValue(COLLECTION_NAME_OPTION); - - echo("\nDeleting core '" + coreName + "' using V2 Cores API\n"); + private void deleteCore(DeleteParams params, SolrClient solrClient) throws Exception { + echo("\nDeleting core '" + params.name + "' using V2 Cores API\n"); try { - var req = new CoresApi.UnloadCore(coreName); + var req = new CoresApi.UnloadCore(params.name); req.setDeleteIndex(true); req.setDeleteDataDir(true); req.setDeleteInstanceDir(true); var response = req.process(solrClient); echoIfVerbose(response); } catch (SolrServerException sse) { - throw new Exception("Failed to delete core '" + coreName + "' due to: " + sse.getMessage()); + throw new Exception( + "Failed to delete core '" + params.name + "' due to: " + sse.getMessage()); } } @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + String zkHostArg = + (connectionOptions != null) ? connectionOptions.zkHost : EnvUtils.getProperty("zkHost"); + String solrUrlArg = (connectionOptions != null) ? connectionOptions.solrUrl : null; + DeleteParams params = + new DeleteParams(name, credentialsOptions.credentials, deleteConfig, force); + + if (zkHostArg != null) { + deleteCollection(zkHostArg, params); + } else { + String resolvedSolrUrl; + if (solrUrlArg != null) { + resolvedSolrUrl = CLIUtils.normalizeSolrUrl(solrUrlArg); + } else { + resolvedSolrUrl = CLIUtils.getDefaultSolrUrl(); + CLIO.err( + "Neither --zk-host or --solr-url parameters, nor ZK_HOST env var provided, so assuming solr url is " + + resolvedSolrUrl + + "."); + } + try (var solrClient = + CLIUtils.getSolrClient(resolvedSolrUrl, credentialsOptions.credentials)) { + Map status = StatusTool.reportStatus(solrClient); + @SuppressWarnings("unchecked") + Map cloud = (Map) status.get("cloud"); + if (cloud != null) { + String zookeeper = (String) cloud.get("ZooKeeper"); + if (zookeeper != null && zookeeper.endsWith("(embedded)")) { + zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length()); + } + deleteCollection(zookeeper, params); + } else { + deleteCore(params, solrClient); + } + } + } + return 0; } } diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index 9f62a90868c3..1f9fc358c977 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -78,7 +78,9 @@ StopCommand.class, StatusTool.class, VersionTool.class, - ZkTool.class + ZkTool.class, + CreateTool.class, + DeleteTool.class }) public class SolrCLI implements CLIO { diff --git a/solr/core/src/java/org/apache/solr/cli/ToolBase.java b/solr/core/src/java/org/apache/solr/cli/ToolBase.java index c05595c94f62..467c7b71583a 100644 --- a/solr/core/src/java/org/apache/solr/cli/ToolBase.java +++ b/solr/core/src/java/org/apache/solr/cli/ToolBase.java @@ -19,16 +19,14 @@ import com.fasterxml.jackson.core.JsonProcessingException; import java.util.concurrent.Callable; -import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.OptionGroup; import org.apache.commons.cli.Options; import org.apache.solr.client.solrj.request.json.JacksonContentWriter; import org.apache.solr.util.StartupLoggingUtils; +import picocli.CommandLine; public abstract class ToolBase implements Tool, Callable { - @picocli.CommandLine.Mixin HelpMixin helpMixin; - - @picocli.CommandLine.Option( + @CommandLine.Option( names = {"-v", "--verbose"}, description = "Enable verbose mode.") private boolean verbose = false; @@ -89,7 +87,7 @@ public OptionGroup getConnectionOptions() { } @Override - public int runTool(CommandLine cli) throws Exception { + public int runTool(org.apache.commons.cli.CommandLine cli) throws Exception { verbose = cli.hasOption(CommonCLIOptions.VERBOSE_OPTION); raiseLogLevelUnlessVerbose(); @@ -119,7 +117,7 @@ private void raiseLogLevelUnlessVerbose() { } @Deprecated - public abstract void runImpl(CommandLine cli) throws Exception; + public abstract void runImpl(org.apache.commons.cli.CommandLine cli) throws Exception; /** * Called by picocli to execute the tool's logic. Each tool must implement this method to support diff --git a/solr/packaging/test/test_create.bats b/solr/packaging/test/test_create.bats index e9199358253e..10b934dc523d 100644 --- a/solr/packaging/test/test_create.bats +++ b/solr/packaging/test/test_create.bats @@ -42,5 +42,5 @@ teardown() { @test "multiple connection options are prevented" { run solr create -c COLL_NAME2 --solr-url http://localhost:${SOLR_PORT} -z localhost:${ZK_PORT} - assert_output --partial "The option 'z' was specified but an option from this group has already been selected: 's'" + assert_output --regexp "(mutually exclusive|already been selected).*(--solr-url|--zk-host|-z|-s)" } From f6d6f5f188da87c83dc2f5f8af1e48b6e3cdf3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 15 May 2026 14:21:18 +0200 Subject: [PATCH 09/19] SOLR-17697 Ref-guide documentation for new picocli commands (#4271) --- dev-docs/ref-guide/antora.adoc | 32 +++ solr/core/build.gradle | 20 ++ .../solr/cli/ConfigSetDownloadTool.java | 10 +- .../apache/solr/cli/ConfigSetUploadTool.java | 10 +- .../java/org/apache/solr/cli/CreateTool.java | 18 +- .../java/org/apache/solr/cli/DeleteTool.java | 15 +- .../src/java/org/apache/solr/cli/SolrCLI.java | 4 +- .../org/apache/solr/cli/StartCommand.java | 15 +- .../java/org/apache/solr/cli/StatusTool.java | 20 +- .../java/org/apache/solr/cli/StopCommand.java | 12 +- .../org/apache/solr/cli/UpdateACLTool.java | 14 +- .../java/org/apache/solr/cli/ZkCpTool.java | 11 + .../java/org/apache/solr/cli/ZkLsTool.java | 12 +- .../org/apache/solr/cli/ZkMkrootTool.java | 10 +- .../java/org/apache/solr/cli/ZkMvTool.java | 8 + .../java/org/apache/solr/cli/ZkRmTool.java | 12 +- solr/solr-ref-guide/build.gradle | 224 ++++++++++++++++++ .../deployment-guide/deployment-nav.adoc | 17 ++ .../pages/cli/solr-create.adoc | 120 ++++++++++ .../pages/cli/solr-delete.adoc | 109 +++++++++ .../pages/cli/solr-start.adoc | 138 +++++++++++ .../pages/cli/solr-status.adoc | 104 ++++++++ .../deployment-guide/pages/cli/solr-stop.adoc | 90 +++++++ .../pages/cli/solr-version.adoc | 66 ++++++ .../pages/cli/solr-zk-cp.adoc | 110 +++++++++ .../pages/cli/solr-zk-downconfig.adoc | 92 +++++++ .../pages/cli/solr-zk-ls.adoc | 93 ++++++++ .../pages/cli/solr-zk-mkroot.adoc | 94 ++++++++ .../pages/cli/solr-zk-mv.adoc | 97 ++++++++ .../pages/cli/solr-zk-rm.adoc | 93 ++++++++ .../pages/cli/solr-zk-upconfig.adoc | 92 +++++++ .../pages/cli/solr-zk-updateacls.adoc | 90 +++++++ .../deployment-guide/pages/cli/solr-zk.adoc | 92 +++++++ .../pages/solr-control-script-reference.adoc | 67 ++++++ 34 files changed, 1998 insertions(+), 13 deletions(-) create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-start.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-status.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-stop.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-version.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk.adoc diff --git a/dev-docs/ref-guide/antora.adoc b/dev-docs/ref-guide/antora.adoc index a363cb3fe46e..7ed1debefc06 100644 --- a/dev-docs/ref-guide/antora.adoc +++ b/dev-docs/ref-guide/antora.adoc @@ -61,6 +61,38 @@ Instead, update `antora.template.yaml`, and `gradlew buildLocalSite` will build The only reason you will likely need to change the `antora.template.yml` is if you are introducing new variables for dependency versions. +== Auto-generated CLI Reference Pages + +The `bin/solr` command-line tool documentation is partially auto-generated from the picocli `@Command`, `@Option`, and `@Parameters` annotations in the Java source. + +=== How it works + +For each command registered in `org.apache.solr.cli.SolrCLI` that uses picocli annotations, a dedicated AsciiDoc page is generated by `picocli.codegen.docgen.manpage.ManPageGenerator` and committed to: + +---- +solr/solr-ref-guide/modules/deployment-guide/pages/cli/ +---- + +The generated pages are post-processed to be Antora-compatible (correct page title, `:page-toclevels:` attribute, ASF license header, DO NOT EDIT notice). +The navigation section in `deployment-nav.adoc` between the `// CLI-DOCS-START` and `// CLI-DOCS-END` markers is also updated automatically. + +The main landing page `solr-control-script-reference.adoc` provides an overview of the available tools with links to each man-page. + +=== Gradle targets + +`./gradlew :solr:solr-ref-guide:generateCliDocs`:: +Regenerates all pages under `pages/cli/` from the current picocli annotations and updates the nav. +Run this after modifying any `@Command`, `@Option`, or `@Parameters` annotation in `solr/core/src/java/org/apache/solr/cli/`. + +`./gradlew :solr:solr-ref-guide:checkCliDocsUpToDate`:: +Verifies that the committed pages in `pages/cli/` are in sync with the current annotations. +This target is wired into `gradlew check`, so CI will catch annotation changes that were not followed by a `generateCliDocs` run. + +=== Adding docs for a new tool + +The new command will automatically appear in the generated pages and the navigation when running `gradlew generateCliDocs`. +You will need to tie it in to the `solr-control-script-reference.adoc` yourself. + == Building the HTML Site A Gradle target `gradlew buildLocalSite` will build the full HTML site (found in `solr/solr-ref-guide/build/site`). diff --git a/solr/core/build.gradle b/solr/core/build.gradle index 398a0a2a2ca4..721aa9678072 100644 --- a/solr/core/build.gradle +++ b/solr/core/build.gradle @@ -210,3 +210,23 @@ dependencies { testImplementation libs.opentelemetry.sdk.testing testImplementation libs.dropwizard.metrics.core } + +// Expose a configuration for CLI doc generation in the solr-ref-guide project. +// This bundles the full runtime classpath plus picocli-codegen (ManPageGenerator) +// and the compiled CLI classes so that generateCliDocs can invoke ManPageGenerator. +configurations { + cliDocsRuntime { + canBeResolved = true + canBeConsumed = true + // Inherit all runtime dependencies (implementation + runtimeOnly transitively) + extendsFrom configurations.runtimeClasspath + } +} + +dependencies { + // picocli-codegen provides ManPageGenerator (not in runtime, only needed for doc gen) + cliDocsRuntime libs.picocli.codegen + // Include the compiled CLI classes themselves + cliDocsRuntime files(sourceSets.main.output.classesDirs) + cliDocsRuntime files(sourceSets.main.output.resourcesDir) +} diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java index 0d812de23fb9..e58f09468d41 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java @@ -31,7 +31,15 @@ /** Supports zk downconfig command in the bin/solr script. */ @picocli.CommandLine.Command( name = "downconfig", - description = "Download a configset from ZooKeeper to the local filesystem.") + description = "Download a configset from ZooKeeper to the local filesystem.", + footerHeading = "%nExamples:%n", + footer = { + " # Download a configset from ZooKeeper", + " bin/solr zk downconfig -n myconfig -d /local/conf -z localhost:9983", + "", + " # Download into a path relative to the Solr installation", + " bin/solr zk downconfig -n myconfig -d server/solr/configsets/myconfig/conf -z localhost:9983" + }) public class ConfigSetDownloadTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java index 156681b48891..c35e6d572574 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java @@ -33,7 +33,15 @@ /** Supports zk upconfig command in the bin/solr script. */ @picocli.CommandLine.Command( name = "upconfig", - description = "Upload a configset from the local filesystem to ZooKeeper.") + description = "Upload a configset from the local filesystem to ZooKeeper.", + footerHeading = "%nExamples:%n", + footer = { + " # Upload a configset to ZooKeeper", + " bin/solr zk upconfig -n myconfig -d /local/conf -z localhost:9983", + "", + " # Upload using a path relative to the Solr installation", + " bin/solr zk upconfig -n myconfig -d server/solr/configsets/myconfig/conf -z localhost:9983" + }) public class ConfigSetUploadTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java b/solr/core/src/java/org/apache/solr/cli/CreateTool.java index ed1995d94345..7668a21215ce 100644 --- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java @@ -47,7 +47,23 @@ name = "create", mixinStandardHelpOptions = true, description = - "Creates a core or collection depending on whether Solr is running in standalone (core) or SolrCloud mode (collection).") + "Creates a core or collection depending on whether Solr is running in standalone (core) or SolrCloud mode (collection).", + exitCodeListHeading = "%nExit Codes:%n", + exitCodeList = { + "0:Collection or core created successfully.", + "1:Failed to create; Solr may not be running, or the collection/core already exists." + }, + footerHeading = "%nExamples:%n", + footer = { + " # Create a collection in SolrCloud mode", + " bin/solr create -c myCollection", + "", + " # Create a collection with 2 shards and 2 replicas", + " bin/solr create -c myCollection -s 2 -rf 2", + "", + " # Create a collection using an existing configset", + " bin/solr create -c myCollection -d myconfig" + }) public class CreateTool extends ToolBase { private static final Option COLLECTION_NAME_OPTION = diff --git a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java index 25a49e4e82b3..059caed81e13 100644 --- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java @@ -42,7 +42,20 @@ name = "delete", mixinStandardHelpOptions = true, description = - "Deletes a collection or core depending on whether Solr is running in SolrCloud or standalone mode.") + "Deletes a collection or core depending on whether Solr is running in SolrCloud or standalone mode.", + exitCodeListHeading = "%nExit Codes:%n", + exitCodeList = { + "0:Collection or core deleted successfully.", + "1:Failed to delete; collection or core may not exist, or Solr may not be running." + }, + footerHeading = "%nExamples:%n", + footer = { + " # Delete a collection in SolrCloud mode", + " bin/solr delete -c myCollection", + "", + " # Delete and also remove the associated configset", + " bin/solr delete -c myCollection --delete-config" + }) public class DeleteTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index 1f9fc358c977..2fdc812c202c 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -55,10 +55,10 @@ /** Command-line utility for working with Solr. */ @picocli.CommandLine.Command( - name = "bin/solr", + name = "solr", version = "Apache Solr version " + SolrVersion.LATEST_STRING, mixinStandardHelpOptions = true, - synopsisHeading = "usage: ", + synopsisHeading = "usage: bin/", commandListHeading = "\nCommands:\n", footer = { "", diff --git a/solr/core/src/java/org/apache/solr/cli/StartCommand.java b/solr/core/src/java/org/apache/solr/cli/StartCommand.java index ed173a42beef..4d1f6f410c76 100644 --- a/solr/core/src/java/org/apache/solr/cli/StartCommand.java +++ b/solr/core/src/java/org/apache/solr/cli/StartCommand.java @@ -19,7 +19,20 @@ import java.util.concurrent.Callable; import picocli.CommandLine; -@CommandLine.Command(name = "start", description = "Starts Solr in standalone or SolrCloud mode.") +@CommandLine.Command( + name = "start", + description = "Starts Solr in standalone or SolrCloud mode.", + footerHeading = "%nExamples:%n", + footer = { + " # Start Solr in SolrCloud mode on the default port", + " bin/solr start", + "", + " # Start on a custom port with 2g heap", + " bin/solr start -p 8984 -m 2g", + "", + " # Start in standalone mode with an external ZooKeeper", + " bin/solr start --user-managed -z localhost:2181/solr" + }) public class StartCommand implements Callable { @CommandLine.Mixin HelpMixin helpMixin; diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index 081bd57bca41..6a9b8b0fe079 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -44,7 +44,25 @@ * *

Get the status of a Solr server. */ -@picocli.CommandLine.Command(name = "status", description = "Get the status of a Solr server.") +@picocli.CommandLine.Command( + name = "status", + description = "Get the status of a Solr server.", + exitCodeListHeading = "%nExit Codes:%n", + exitCodeList = { + "0:Solr is running.", + "1:Solr is not running, or the URL/port could not be reached." + }, + footerHeading = "%nExamples:%n", + footer = { + " # Check status of Solr running on the default port", + " bin/solr status", + "", + " # Check status of a specific Solr instance by URL", + " bin/solr status -s http://localhost:8983", + "", + " # Wait up to 30 seconds for Solr to start", + " bin/solr status --max-wait-secs 30 -s http://localhost:8983" + }) public class StatusTool extends ToolBase { @picocli.CommandLine.Option( names = {"--max-wait-secs"}, diff --git a/solr/core/src/java/org/apache/solr/cli/StopCommand.java b/solr/core/src/java/org/apache/solr/cli/StopCommand.java index 0803cea767ac..b69d7771f0b2 100644 --- a/solr/core/src/java/org/apache/solr/cli/StopCommand.java +++ b/solr/core/src/java/org/apache/solr/cli/StopCommand.java @@ -22,7 +22,17 @@ * This class is currently only used for printing CLI usage. The stop logic is currently handled in * start script. */ -@CommandLine.Command(name = "stop", description = "Stops Solr.") +@CommandLine.Command( + name = "stop", + description = "Stops Solr.", + footerHeading = "%nExamples:%n", + footer = { + " # Stop Solr running on the default port", + " bin/solr stop -p 8983", + "", + " # Stop all running Solr instances on this host", + " bin/solr stop --all" + }) public class StopCommand { @CommandLine.Mixin HelpMixin helpMixin; diff --git a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java index c0ebbe449022..a7a8dee64038 100644 --- a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java +++ b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java @@ -31,7 +31,19 @@ */ @picocli.CommandLine.Command( name = "updateacls", - description = "Update ACLs for a ZooKeeper znode.") + description = + "Recursively re-applies ZooKeeper ACLs to a znode and all its descendants." + + " The ACLs are determined by the ZkACLProvider configured via the zkACLProvider" + + " system property (pass via SOLR_TOOL_OPTS). Without a custom provider, the" + + " default OPEN_ACL_UNSAFE ACLs are applied.", + footerHeading = "%nExamples:%n", + footer = { + " # Apply ACLs to the root Solr znode", + " bin/solr zk updateacls /solr -z localhost:9983", + "", + " # Apply ACLs to a specific configset", + " bin/solr zk updateacls /configs/myconfig -z localhost:9983" + }) public class UpdateACLTool extends ToolBase { // It is a shame this tool doesn't more closely mimic how the ConfigTool works. diff --git a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java index 292f88c39f41..ffdce4ef6a5f 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java @@ -49,6 +49,17 @@ "When is a zk resource, may be '.'", "If ends with '/', then will be a local folder or parent znode", "and the last element of the path will be appended unless also ends in a slash." + }, + footerHeading = "%nExamples:%n", + footer = { + " # Upload a local file to ZooKeeper", + " bin/solr zk cp /local/conf/solrconfig.xml zk:/configs/myconfig/solrconfig.xml -z localhost:9983", + "", + " # Recursively copy a local directory to ZooKeeper", + " bin/solr zk cp -r file:/local/conf/ zk:/configs/myconfig/ -z localhost:9983", + "", + " # Copy a ZooKeeper node to the current directory", + " bin/solr zk cp zk:/configs/myconfig/solrconfig.xml . -z localhost:9983" }) public class ZkCpTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java index 56e4ef21b7de..ae87c1173d92 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkLsTool.java @@ -26,7 +26,17 @@ import picocli.CommandLine; /** Supports zk ls command in the bin/solr script. */ -@CommandLine.Command(name = "ls", description = "List the contents of a ZooKeeper node.") +@CommandLine.Command( + name = "ls", + description = "List the contents of a ZooKeeper node.", + footerHeading = "%nExamples:%n", + footer = { + " # List top-level ZooKeeper nodes", + " bin/solr zk ls /configs -z localhost:9983", + "", + " # Recursively list a configset", + " bin/solr zk ls -r /configs/myconfig -z localhost:9983" + }) public class ZkLsTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java index 82573f74dc58..eec379b1d565 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java @@ -33,7 +33,15 @@ name = "mkroot", description = "Make a znode in ZooKeeper with no data. Can be used to make a path of arbitrary depth" - + " but primarily intended to create a 'chroot'.") + + " but primarily intended to create a 'chroot'.", + footerHeading = "%nExamples:%n", + footer = { + " # Create a chroot path for Solr", + " bin/solr zk mkroot /solr -z localhost:2181", + "", + " # Create a nested chroot path", + " bin/solr zk mkroot /solr/myapp -z localhost:2181" + }) public class ZkMkrootTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java index 9036e461bfc8..ca9c1f50a5aa 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java @@ -38,6 +38,14 @@ ", : ZooKeeper nodes, the 'zk:' prefix is optional.", "If ends with '/', then will be a parent znode", "and the last element of the path will be appended." + }, + footerHeading = "%nExamples:%n", + footer = { + " # Rename a configset", + " bin/solr zk mv /configs/oldconfig /configs/myconfig -z localhost:9983", + "", + " # Move a node into a parent znode", + " bin/solr zk mv /configs/myconfig /configs/ -z localhost:9983" }) public class ZkMvTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java index ed529066a108..cf700a8b8fd3 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java @@ -28,7 +28,17 @@ import org.slf4j.LoggerFactory; /** Supports zk rm command in the bin/solr script. */ -@picocli.CommandLine.Command(name = "rm", description = "Remove a znode from ZooKeeper.") +@picocli.CommandLine.Command( + name = "rm", + description = "Remove a znode from ZooKeeper.", + footerHeading = "%nExamples:%n", + footer = { + " # Remove a single file from ZooKeeper", + " bin/solr zk rm /configs/myconfig/solrconfig.xml -z localhost:9983", + "", + " # Recursively remove a configset", + " bin/solr zk rm -r /configs/myconfig -z localhost:9983" + }) public class ZkRmTool extends ToolBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); diff --git a/solr/solr-ref-guide/build.gradle b/solr/solr-ref-guide/build.gradle index 5b53514d698a..5b9dc95bd89f 100644 --- a/solr/solr-ref-guide/build.gradle +++ b/solr/solr-ref-guide/build.gradle @@ -60,11 +60,16 @@ configurations { officialPlaybook localPlaybook localJavadocs + cliClasspath { + canBeResolved = true + canBeConsumed = false + } } dependencies { localJavadocs project(path: ":solr:documentation", configuration: 'javadocs') localJavadocs project(path: ":solr:documentation", configuration: 'site') + cliClasspath project(path: ':solr:core', configuration: 'cliDocsRuntime') } ext { @@ -540,6 +545,225 @@ task buildOfficialSite(type: NpxTask) { } } +/* + CLI Documentation Generation from Picocli Annotations + Generates per-command AsciiDoc reference pages for the Antora ref guide. + Usage: + ./gradlew :solr:solr-ref-guide:generateCliDocs -- regenerate pages/cli/ + ./gradlew :solr:solr-ref-guide:checkCliDocsUpToDate -- verify in-sync (also run by check) + */ + +def ASF_LICENSE_HEADER = """\ +// 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. +""" + +def DO_NOT_EDIT_NOTICE = """\ +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs +""" + +// Convert a generated filename stem (e.g. "solr-zk-ls") to an Antora page title +// e.g. "solr-zk-ls" -> "bin/solr zk ls" +String cliFileNameToTitle(String baseName) { + def parts = baseName.split('-') + return "bin/solr " + parts.drop(1).join(' ') +} + +// Return the short nav label for a command file (last segment of dash-separated name) +// e.g. "solr-zk-ls" -> "ls" +String cliFileNameToNavLabel(String baseName) { + return baseName.split('-').last() +} + +// Post-process a raw ManPageGenerator AsciiDoc file for Antora compatibility. +// The title lives inside the man-section-header block, so we replace the whole +// header block with an Antora-compatible page title + attributes. +String postProcessCliManPage(File rawFile, String asfHeader, String doNotEditNotice) { + def content = rawFile.text + def baseName = rawFile.name.replace('.adoc', '') + def newTitle = cliFileNameToTitle(baseName) + + // Replace the entire man-section-header block (which contains :doctype:manpage, the page title + // "= solr-start(1)", etc.) with our Antora-compatible title and page attributes. + content = content.replaceAll( + /(?s)\/\/ tag::picocli-generated-man-section-header\[\].*?\/\/ end::picocli-generated-man-section-header\[\]\n\n?/, + "= ${newTitle}\n:page-toclevels: 2\n\n") + + // Fix xref paths: ManPageGenerator generates "xref:solr-*.adoc" but in Antora xrefs are resolved + // from the module's pages/ root, so pages/cli/ files need the "cli/" prefix. + content = content.replace('xref:solr-', 'xref:cli/solr-') + + // ManPageGenerator renders the command's file-stem as "solr-start" (parent-dash-child) in the + // Name section. Replace occurrences of that with the correct spaced form "bin/solr start". + // Use a negative lookahead (?!-) to avoid replacing "solr-zk" inside xref paths like + // "xref:cli/solr-zk-downconfig.adoc" where it is a prefix of a longer command name. + content = content.replaceAll("${baseName}(?!-)", newTitle) + + // Wrap the synopsis paragraph in a literal block to preserve line breaks and indentation. + // ManPageGenerator generates a plain paragraph with picocli AsciiDoc marks (*bold*, _italic_) + // which Asciidoctor collapses to a single line. A `....` literal block preserves whitespace + // but does not render inline formatting, so we strip the marks for a clean monospace output. + content = content.replaceAll( + /(?s)(== Synopsis\n\n)(.*?)(\n\n\/\/ end::picocli-generated-man-section-synopsis\[\])/ + ) { List m -> + def synopsis = m[2] + .replaceAll(/\*([^*]+)\*/, '$1') // *bold* → plain + .replaceAll(/_([^_]+)_/, '$1') // _italic_ → plain + .replaceAll(/(?m)^solr /, 'bin/solr ') // qualified name "solr start" → "bin/solr start" + .replaceAll(/(?m)^( +)/, ' $1') // re-align wrapped lines: add 4 spaces ("bin/" length) + "${m[1]}....\n${synopsis}\n....${m[3]}" + } + + // Strip the outer full-manpage tag wrappers + content = content.replace("// tag::picocli-generated-full-manpage[]\n", '') + content = content.replace("// end::picocli-generated-full-manpage[]\n", '') + + return asfHeader + doNotEditNotice + "\n" + content.stripTrailing() + "\n" +} + +// Build the CLI sub-page nav entries from the generated .adoc files in destDir. +// Returns a list of nav lines like "** xref:cli/solr-start.adoc[start]" +List buildCliNavEntries(File cliDir) { + // Sort so that parent commands always precede their children. + // The trick: append '\u0000' (NUL, ASCII 0) to the stem before sorting. + // NUL < '-' (ASCII 45), so "solr-zk\0" < "solr-zk-cp\0", placing the parent first. + def files = ((cliDir.listFiles() ?: []) as List) + .findAll { it.name.endsWith('.adoc') } + .sort { (it.name.replace('.adoc', '') + '\u0000') } + return files.collect { file -> + def baseName = file.name.replace('.adoc', '') + def parts = baseName.split('-') + // depth: "solr-start" has 2 parts → "**", "solr-zk-ls" has 3 parts → "***" + def stars = '*' * parts.length + def label = cliFileNameToNavLabel(baseName) + "${stars} xref:cli/${file.name}[${label}]" + } +} + +// Run ManPageGenerator and return the set of generated files (excluding the top-level solr.adoc). +// SolrCLI is annotated @Command(name="solr"), so ManPageGenerator writes all files flat into outputDir. +List runManPageGenerator(File outputDir) { + javaexec { + classpath = configurations.cliClasspath + mainClass = 'picocli.codegen.docgen.manpage.ManPageGenerator' + args = ['--outdir', outputDir.absolutePath, 'org.apache.solr.cli.SolrCLI'] + } + + // Exclude solr.adoc (the top-level parent page — we don't publish it as a standalone page) + return (outputDir.listFiles() as List) + .findAll { f -> f.name.endsWith('.adoc') && f.name != 'solr.adoc' } + .sort { it.name } +} + +task generateCliDocs { + group = 'generation' + description = 'Regenerate CLI reference pages in pages/cli/ from picocli annotations.' + + dependsOn ':solr:core:classes' + + def rawOutputDir = file("${buildDir}/generated-cli-docs-raw") + def destDir = file("modules/deployment-guide/pages/cli") + def navFile = file("modules/deployment-guide/deployment-nav.adoc") + + inputs.files(configurations.cliClasspath) + outputs.dir(destDir) + + doLast { + rawOutputDir.mkdirs() + destDir.mkdirs() + + def rawFiles = runManPageGenerator(rawOutputDir) + + rawFiles.each { rawFile -> + def processed = postProcessCliManPage(rawFile, ASF_LICENSE_HEADER, DO_NOT_EDIT_NOTICE) + new File(destDir, rawFile.name).text = processed + } + + // Update the CLI nav section in deployment-nav.adoc between marker comments + def navContent = navFile.text + if (navContent.contains('// CLI-DOCS-START')) { + def navEntries = buildCliNavEntries(destDir).join('\n') + def replacement = "// CLI-DOCS-START\n* xref:solr-control-script-reference.adoc[]\n${navEntries}\n// CLI-DOCS-END" + navContent = navContent.replaceAll(/(?s)\/\/ CLI-DOCS-START.*?\/\/ CLI-DOCS-END/, replacement) + navFile.text = navContent + } else { + logger.warn("deployment-nav.adoc does not contain // CLI-DOCS-START marker; nav not updated.") + } + + logger.lifecycle("Generated ${rawFiles.size()} CLI doc pages in ${destDir}") + } +} + +task checkCliDocsUpToDate { + group = 'verification' + description = 'Verify that pages/cli/ CLI docs are in sync with picocli annotations.' + + dependsOn ':solr:core:classes' + + def rawOutputDir = file("${buildDir}/generated-cli-docs-check") + def committedDir = file("modules/deployment-guide/pages/cli") + + inputs.files(configurations.cliClasspath) + inputs.dir(committedDir) + + doLast { + rawOutputDir.mkdirs() + + def rawFiles = runManPageGenerator(rawOutputDir) + def issues = [] + + rawFiles.each { rawFile -> + def processed = postProcessCliManPage(rawFile, ASF_LICENSE_HEADER, DO_NOT_EDIT_NOTICE) + def committed = new File(committedDir, rawFile.name) + if (!committed.exists()) { + issues << "MISSING committed file (new command): ${rawFile.name}" + } else { + def processedNorm = processed.replace('\r\n', '\n') + def committedNorm = committed.text.replace('\r\n', '\n') + if (processedNorm != committedNorm) { + issues << "OUT OF DATE: ${rawFile.name}" + } + } + } + + // Check for committed pages that are no longer generated (deleted commands) + if (committedDir.exists()) { + def generatedNames = rawFiles.collect { it.name }.toSet() + ((committedDir.listFiles() ?: []) as List).findAll { it.name.endsWith('.adoc') }.each { committed -> + if (!generatedNames.contains(committed.name)) { + issues << "STALE (command removed): ${committed.name}" + } + } + } + + if (!issues.isEmpty()) { + throw new GradleException( + "CLI docs are out of date. Run './gradlew :solr:solr-ref-guide:generateCliDocs' to regenerate.\n" + + "Issues:\n" + issues.collect { " - ${it}" }.join('\n')) + } + logger.lifecycle("CLI docs are up to date.") + } +} + +check.dependsOn checkCliDocsUpToDate + /* Compiling, Testing and Validation for the java examples in the Solr Ref Guide */ diff --git a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc index 55301601e3e0..89b892de4e09 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc @@ -17,7 +17,24 @@ .Deployment Guide +// CLI-DOCS-START * xref:solr-control-script-reference.adoc[] +** xref:cli/solr-create.adoc[create] +** xref:cli/solr-delete.adoc[delete] +** xref:cli/solr-start.adoc[start] +** xref:cli/solr-status.adoc[status] +** xref:cli/solr-stop.adoc[stop] +** xref:cli/solr-version.adoc[version] +** xref:cli/solr-zk.adoc[zk] +*** xref:cli/solr-zk-cp.adoc[cp] +*** xref:cli/solr-zk-downconfig.adoc[downconfig] +*** xref:cli/solr-zk-ls.adoc[ls] +*** xref:cli/solr-zk-mkroot.adoc[mkroot] +*** xref:cli/solr-zk-mv.adoc[mv] +*** xref:cli/solr-zk-rm.adoc[rm] +*** xref:cli/solr-zk-upconfig.adoc[upconfig] +*** xref:cli/solr-zk-updateacls.adoc[updateacls] +// CLI-DOCS-END * Installation & Deployment ** xref:system-requirements.adoc[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc new file mode 100644 index 000000000000..47112c051273 --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc @@ -0,0 +1,120 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr create +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr create - Creates a core or collection depending on whether Solr is running in standalone (core) or SolrCloud mode (collection). + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr create [-hvV] -c= [-d=] [-n=] + [-rf=] [-sh=] [-u=] + [-s= | -z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Creates a core or collection depending on whether Solr is running in standalone (core) or SolrCloud mode (collection). + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-c*, *--name*=__:: + Name of collection or core to create. + +*-d*, *--conf-dir*=__:: + Configuration directory to copy when creating the new collection; default is _default. + +*-h*, *--help*:: + Show this help message and exit. + +*-n*, *--conf-name*=__:: + Configuration name; default is the collection name. + +*-rf, --replication-factor*=__:: + Number of copies of each document across the collection (replicas per shard); default is 1. + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if that's not known. + +*-sh, --shards*=__:: + Number of shards; default is 1. + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-V*, *--version*:: + Print version information and exit. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +== Exit Codes: + +*0*:: + Collection or core created successfully. + +*1*:: + Failed to create; Solr may not be running, or the collection/core already exists. + +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Create a collection in SolrCloud mode + bin/solr create -c myCollection + +[%hardbreaks] + # Create a collection with 2 shards and 2 replicas + bin/solr create -c myCollection -s 2 -rf 2 + +[%hardbreaks] + # Create a collection using an existing configset + bin/solr create -c myCollection -d myconfig + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc new file mode 100644 index 000000000000..1f8a50e52797 --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc @@ -0,0 +1,109 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr delete +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr delete - Deletes a collection or core depending on whether Solr is running in SolrCloud or standalone mode. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr delete [-fhvV] [--delete-config] -c= [-u=] + [-s= | -z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Deletes a collection or core depending on whether Solr is running in SolrCloud or standalone mode. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-c*, *--name*=__:: + Name of the core / collection to delete. + +*--delete-config*:: + Flag to indicate if the underlying configuration directory for a collection should also be deleted; default is false. + +*-f*, *--force*:: + Skip safety checks when deleting the configuration directory used by a collection. + +*-h*, *--help*:: + Show this help message and exit. + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if that's not known. + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-V*, *--version*:: + Print version information and exit. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +== Exit Codes: + +*0*:: + Collection or core deleted successfully. + +*1*:: + Failed to delete; collection or core may not exist, or Solr may not be running. + +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Delete a collection in SolrCloud mode + bin/solr delete -c myCollection + +[%hardbreaks] + # Delete and also remove the associated configset + bin/solr delete -c myCollection --delete-config + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-start.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-start.adoc new file mode 100644 index 000000000000..15752e38cfea --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-start.adoc @@ -0,0 +1,138 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr start +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr start - Starts Solr in standalone or SolrCloud mode. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr start [-fhqy] [--force] [--user-managed] [--verbose] + [--data-home=] [-e=] [--example-dir=] + [--host=] [-j=] [--jvm-opts=] + [-m=] [-p=] [--prompt-inputs=] + [--server-dir=] [--solr-home=] [-z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Starts Solr in standalone or SolrCloud mode. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*--data-home*=__:: + Set solr.data.home system property for index data storage; default is solr.solr.home + +*-e*, *--example*=__:: + Run an example: cloud, techproducts, schemaless, films + +*--example-dir*=__:: + Override the directory containing example configurations used when running examples with --example + +*-f*, *--foreground*:: + Start Solr in foreground; default is background with logs to solr-PORT-console.log + +*--force*:: + Override warning when attempting to start Solr as root user + +*-h*, *--help*:: + Print this help message and exit. + +*--host*=__:: + Specify the hostname for this Solr instance + +*-j*, *--jettyconfig*=__:: + Additional Jetty parameters, e.g., -j "--include-jetty-dir=/etc/jetty/custom/server/" + +*--jvm-opts*=__:: + Additional JVM parameters, e.g., --jvm-opts "-verbose:gc" + +*-m*, *--memory*=__:: + Set JVM heap size, e.g., -m 4g sets -Xms4g -Xmx4g; default is 512m + +*-p*, *--port*=__:: + Specify the Solr HTTP port; default is 8983. STOP_PORT=($SOLR_PORT-1000), RMI_PORT=($SOLR_PORT+10000) + +*--prompt-inputs*=__:: + Don't prompt for input; comma-delimited list of inputs to use when running examples that accept user input + +*-q*, *--quiet*:: + Set log level to WARN (quiet); default is INFO + +*--server-dir*=__:: + Specify the Solr server directory; default is 'server' + +*--solr-home*=__:: + Set solr.solr.home system property; default is 'server/solr'. Ignored in examples mode + +*--user-managed*:: + Start Solr in standalone mode. Default is SolrCloud (ZooKeeper) mode. + +*--verbose*:: + Set log level to DEBUG (verbose); default is INFO + +*-y*, *--no-prompt*:: + Don't prompt for input; accept all defaults when running examples + +*-z*, *--zk-host*=__:: + Zookeeper connection string; default is to start an embedded ZooKeeper on PORT+10000 + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Start Solr in SolrCloud mode on the default port + bin/solr start + +[%hardbreaks] + # Start on a custom port with 2g heap + bin/solr start -p 8984 -m 2g + +[%hardbreaks] + # Start in standalone mode with an external ZooKeeper + bin/solr start --user-managed -z localhost:2181/solr + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-status.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-status.adoc new file mode 100644 index 000000000000..59c45857277a --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-status.adoc @@ -0,0 +1,104 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr status +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr status - Get the status of a Solr server. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr status [-v] [--short] [--max-wait-secs=] [-p=] + [-s=] [-u=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Get the status of a Solr server. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*--max-wait-secs*=__:: + Wait up to the specified number of seconds to see Solr running. + +*-p*, *--port*=__:: + Port on localhost to check status for + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if that's not known + +*--short*:: + Short format. Prints one URL per line for running instances + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +== Exit Codes: + +*0*:: + Solr is running. + +*1*:: + Solr is not running, or the URL/port could not be reached. + +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Check status of Solr running on the default port + bin/solr status + +[%hardbreaks] + # Check status of a specific Solr instance by URL + bin/solr status -s http://localhost:8983 + +[%hardbreaks] + # Wait up to 30 seconds for Solr to start + bin/solr status --max-wait-secs 30 -s http://localhost:8983 + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-stop.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-stop.adoc new file mode 100644 index 000000000000..10f278f85a96 --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-stop.adoc @@ -0,0 +1,90 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr stop +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr stop - Stops Solr. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr stop [-h] [--all] [--verbose] [-k=] [-p=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Stops Solr. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*--all*:: + Find and stop all running Solr servers on this host + +*-h*, *--help*:: + Print this help message and exit. + +*-k*, *--key*=__:: + Stop key; default is solrrocks + +*-p*, *--port*=__:: + Specify the port the Solr HTTP listener is bound to. ++ +The STOP_PORT is derived as ($SOLR_PORT-1000). + +*--verbose*:: + Enable verbose mode. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Stop Solr running on the default port + bin/solr stop -p 8983 + +[%hardbreaks] + # Stop all running Solr instances on this host + bin/solr stop --all + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-version.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-version.adoc new file mode 100644 index 000000000000..806c9ea31856 --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-version.adoc @@ -0,0 +1,66 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr version +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr version - Prints the Solr version. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr version [-v] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Prints the Solr version. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-v*, *--verbose*:: + Enable verbose mode. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc new file mode 100644 index 000000000000..38c55fec065a --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc @@ -0,0 +1,110 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr zk cp +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr zk cp - Copy files or folders to/from ZooKeeper or ZooKeeper to ZooKeeper. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr zk cp [-rv] [-s=] [--solr-home=DIR] [-u=] + [-z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Copy files or folders to/from ZooKeeper or ZooKeeper to ZooKeeper. + +, : [file:][/]path/to/local/file or zk:/path/to/zk/node +NOTE: and may both be ZooKeeper resources prefixed by 'zk:' +When is a zk resource, may be '.' +If ends with '/', then will be a local folder or parent znode +and the last element of the path will be appended unless also ends in a slash. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-r*, *--recursive*:: + Apply the command recursively. + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if --zk-host is not known + +*--solr-home*=_DIR_:: + Required to look up configuration for compressing state.json. + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +== Arguments + +__:: + Source path: [file:][/]path/to/local/file or zk:/path/to/zk/node. + +__:: + Destination path: [file:][/]path/to/local/file or zk:/path/to/zk/node. + +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Upload a local file to ZooKeeper + bin/solr zk cp /local/conf/solrconfig.xml zk:/configs/myconfig/solrconfig.xml -z localhost:9983 + +[%hardbreaks] + # Recursively copy a local directory to ZooKeeper + bin/solr zk cp -r file:/local/conf/ zk:/configs/myconfig/ -z localhost:9983 + +[%hardbreaks] + # Copy a ZooKeeper node to the current directory + bin/solr zk cp zk:/configs/myconfig/solrconfig.xml . -z localhost:9983 + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc new file mode 100644 index 000000000000..65c2b5de424d --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc @@ -0,0 +1,92 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr zk downconfig +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr zk downconfig - Download a configset from ZooKeeper to the local filesystem. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr zk downconfig [-v] -d=DIR -n= [-s=] [-u=] + [-z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Download a configset from ZooKeeper to the local filesystem. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-d*, *--conf-dir*=_DIR_:: + Local directory with configs. + +*-n*, *--conf-name*=__:: + Configset name in ZooKeeper. + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if --zk-host is not known + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Download a configset from ZooKeeper + bin/solr zk downconfig -n myconfig -d /local/conf -z localhost:9983 + +[%hardbreaks] + # Download into a path relative to the Solr installation + bin/solr zk downconfig -n myconfig -d server/solr/configsets/myconfig/conf -z localhost:9983 + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc new file mode 100644 index 000000000000..4c5d82be3d6a --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc @@ -0,0 +1,93 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr zk ls +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr zk ls - List the contents of a ZooKeeper node. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr zk ls [-rv] [-s=] [-u=] [-z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +List the contents of a ZooKeeper node. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-r*, *--recursive*:: + Apply the command recursively. + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if --zk-host is not known + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +== Arguments + +__:: + The path of the ZooKeeper znode path to list. + +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # List top-level ZooKeeper nodes + bin/solr zk ls /configs -z localhost:9983 + +[%hardbreaks] + # Recursively list a configset + bin/solr zk ls -r /configs/myconfig -z localhost:9983 + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc new file mode 100644 index 000000000000..5df27141a8de --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc @@ -0,0 +1,94 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr zk mkroot +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr zk mkroot - Make a znode in ZooKeeper with no data. Can be used to make a path of arbitrary depth but primarily intended to create a 'chroot'. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr zk mkroot [-v] [--fail-on-exists[=]] [-s=] + [-u=] [-z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Make a znode in ZooKeeper with no data. Can be used to make a path of arbitrary depth but primarily intended to create a 'chroot'. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*--fail-on-exists*[=__]:: + Raise an error if the znode already exists. Defaults to false. + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if --zk-host is not known + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +== Arguments + +__:: + The ZooKeeper znode path to create. + +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Create a chroot path for Solr + bin/solr zk mkroot /solr -z localhost:2181 + +[%hardbreaks] + # Create a nested chroot path + bin/solr zk mkroot /solr/myapp -z localhost:2181 + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc new file mode 100644 index 000000000000..46279f49fd95 --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc @@ -0,0 +1,97 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr zk mv +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr zk mv - Move (rename) a znode on ZooKeeper. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr zk mv [-v] [-s=] [-u=] [-z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Move (rename) a znode on ZooKeeper. + +, : ZooKeeper nodes, the 'zk:' prefix is optional. +If ends with '/', then will be a parent znode +and the last element of the path will be appended. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if --zk-host is not known + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +== Arguments + +__:: + Source ZooKeeper znode path (zk: prefix optional). + +__:: + Destination ZooKeeper znode path (zk: prefix optional). + +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Rename a configset + bin/solr zk mv /configs/oldconfig /configs/myconfig -z localhost:9983 + +[%hardbreaks] + # Move a node into a parent znode + bin/solr zk mv /configs/myconfig /configs/ -z localhost:9983 + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc new file mode 100644 index 000000000000..c752f4100360 --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc @@ -0,0 +1,93 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr zk rm +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr zk rm - Remove a znode from ZooKeeper. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr zk rm [-rv] [-s=] [-u=] [-z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Remove a znode from ZooKeeper. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-r*, *--recursive*:: + Apply the command recursively. + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if --zk-host is not known + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +== Arguments + +__:: + The ZooKeeper znode path to remove (zk: prefix optional). + +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Remove a single file from ZooKeeper + bin/solr zk rm /configs/myconfig/solrconfig.xml -z localhost:9983 + +[%hardbreaks] + # Recursively remove a configset + bin/solr zk rm -r /configs/myconfig -z localhost:9983 + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc new file mode 100644 index 000000000000..0b796355c9a0 --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc @@ -0,0 +1,92 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr zk upconfig +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr zk upconfig - Upload a configset from the local filesystem to ZooKeeper. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr zk upconfig [-v] -d=DIR -n= [-s=] [-u=] + [-z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Upload a configset from the local filesystem to ZooKeeper. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-d*, *--conf-dir*=_DIR_:: + Local directory with configs. + +*-n*, *--conf-name*=__:: + Configset name in ZooKeeper. + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if --zk-host is not known + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Upload a configset to ZooKeeper + bin/solr zk upconfig -n myconfig -d /local/conf -z localhost:9983 + +[%hardbreaks] + # Upload using a path relative to the Solr installation + bin/solr zk upconfig -n myconfig -d server/solr/configsets/myconfig/conf -z localhost:9983 + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc new file mode 100644 index 000000000000..e85f005325f0 --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc @@ -0,0 +1,90 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr zk updateacls +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr zk updateacls - Recursively re-applies ZooKeeper ACLs to a znode and all its descendants. The ACLs are determined by the ZkACLProvider configured via the zkACLProvider system property (pass via SOLR_TOOL_OPTS). Without a custom provider, the default OPEN_ACL_UNSAFE ACLs are applied. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr zk updateacls [-v] [-s=] [-u=] [-z=] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Recursively re-applies ZooKeeper ACLs to a znode and all its descendants. The ACLs are determined by the ZkACLProvider configured via the zkACLProvider system property (pass via SOLR_TOOL_OPTS). Without a custom provider, the default OPEN_ACL_UNSAFE ACLs are applied. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-s*, *--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if --zk-host is not known + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*-v*, *--verbose*:: + Enable verbose mode. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +== Arguments + +__:: + The ZooKeeper znode path to update ACLs for. + +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Apply ACLs to the root Solr znode + bin/solr zk updateacls /solr -z localhost:9983 + +[%hardbreaks] + # Apply ACLs to a specific configset + bin/solr zk updateacls /configs/myconfig -z localhost:9983 + +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk.adoc new file mode 100644 index 000000000000..7906fc3d536e --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk.adoc @@ -0,0 +1,92 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr zk +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr zk - Sub commands for working with ZooKeeper. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr zk [-h] [COMMAND] +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Sub commands for working with ZooKeeper. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*-h*, *--help*:: + Print this help message and exit. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +== Commands + +xref:cli/solr-zk-downconfig.adoc[*downconfig*]:: + Download a configset from ZooKeeper to the local filesystem. + +xref:cli/solr-zk-upconfig.adoc[*upconfig*]:: + Upload a configset from the local filesystem to ZooKeeper. + +xref:cli/solr-zk-cp.adoc[*cp*]:: + Copy files or folders to/from ZooKeeper or ZooKeeper to ZooKeeper. + +xref:cli/solr-zk-ls.adoc[*ls*]:: + List the contents of a ZooKeeper node. + +xref:cli/solr-zk-mkroot.adoc[*mkroot*]:: + Make a znode in ZooKeeper with no data. Can be used to make a path of arbitrary depth but primarily intended to create a 'chroot'. + +xref:cli/solr-zk-mv.adoc[*mv*]:: + Move (rename) a znode on ZooKeeper. + +xref:cli/solr-zk-rm.adoc[*rm*]:: + Remove a znode from ZooKeeper. + +xref:cli/solr-zk-updateacls.adoc[*updateacls*]:: + Recursively re-applies ZooKeeper ACLs to a znode and all its descendants. The ACLs are determined by the ZkACLProvider configured via the zkACLProvider system property (pass via SOLR_TOOL_OPTS). Without a custom provider, the default OPEN_ACL_UNSAFE ACLs are applied. + +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +// end::picocli-generated-man-section-footer[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc index a7348059cc2e..7da11daa5085 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc @@ -25,6 +25,47 @@ The SolrCLI makes Solr easier to work with by providing simple commands and opti More examples of SolrCLI in use are available throughout this Guide, particularly in the sections xref:installing-solr.adoc#starting-solr[Starting Solr] and xref:getting-started:tutorial-solrcloud.adoc[]. +== Command Reference + +The table below lists all available `bin/solr` commands. +Commands marked with (✓) have dedicated reference pages auto-generated from source annotations; others are documented inline below. + +[cols="1,1,3",options="header"] +|=== +|Command |Sub-command |Description + +|xref:cli/solr-start.adoc[start] |(✓) |Start Solr +|restart | |Restart a running Solr node +|xref:cli/solr-stop.adoc[stop] |(✓) |Stop a running Solr node +|xref:cli/solr-status.adoc[status] |(✓) |Display status of running Solr nodes +|xref:cli/solr-version.adoc[version] |(✓) |Print Solr version +|create | |Create a collection or core +|delete | |Delete a collection or core +|healthcheck | |Generate a JSON-formatted health report for a collection +|assert | |Assert conditions about the Solr installation +|auth | |Configure authentication +|config | |Set or unset configuration properties +|export | |Export documents from a collection +|import | |Import documents into a collection +|api | |Interact with the Solr API +|package | |Manage Solr packages +|postlogs | |Index log data +|stream | |Execute a streaming expression +|snapshot-create | |Create a snapshot of a collection +|snapshot-delete | |Delete a snapshot +|snapshot-list | |List available snapshots +|snapshot-describe | |Describe a snapshot +|snapshot-export | |Export a snapshot +|xref:cli/solr-zk.adoc[zk] |xref:cli/solr-zk-upconfig.adoc[upconfig] (✓) |Upload a configset to ZooKeeper +|xref:cli/solr-zk.adoc[zk] |xref:cli/solr-zk-downconfig.adoc[downconfig] (✓) |Download a configset from ZooKeeper +|xref:cli/solr-zk.adoc[zk] |xref:cli/solr-zk-cp.adoc[cp] (✓) |Copy files to/from ZooKeeper +|xref:cli/solr-zk.adoc[zk] |xref:cli/solr-zk-ls.adoc[ls] (✓) |List ZooKeeper node contents +|xref:cli/solr-zk.adoc[zk] |xref:cli/solr-zk-rm.adoc[rm] (✓) |Remove a ZooKeeper path +|xref:cli/solr-zk.adoc[zk] |xref:cli/solr-zk-mv.adoc[mv] (✓) |Move a ZooKeeper node +|xref:cli/solr-zk.adoc[zk] |xref:cli/solr-zk-mkroot.adoc[mkroot] (✓) |Create a root ZooKeeper path +|xref:cli/solr-zk.adoc[zk] |xref:cli/solr-zk-updateacls.adoc[updateacls] (✓) |Update ZooKeeper ACLs +|=== + == Starting and Stopping === Start and Restart @@ -46,6 +87,8 @@ When using the `restart` command, you must pass all of the parameters you initia Behind the scenes, a stop request is initiated, so Solr will be stopped before being started again. If no nodes are already running, restart will skip the step to stop and proceed to starting Solr. +For a full list of options see xref:cli/solr-start.adoc[]. + ==== Start Parameters The `bin/solr` script provides many options to allow you to customize the server in common ways, such as changing the listening port. @@ -394,6 +437,8 @@ The command will wait up to 180 seconds for Solr to stop gracefully and then wil `bin/solr stop --help` +For a full list of options see xref:cli/solr-stop.adoc[]. + ==== Stop Parameters `-p `:: @@ -453,6 +498,8 @@ $ bin/solr --version X.Y.0 ---- +For a full list of options see xref:cli/solr-version.adoc[]. + === Status The `status` command displays basic JSON-formatted status information for all locally running Solr servers. @@ -492,6 +539,8 @@ Solr process 39827 running on port 8865 "collections":"2"}} ---- +For a full list of options see xref:cli/solr-status.adoc[]. + === Assert The `assert` command checks common issues with Solr installations. @@ -1167,6 +1216,24 @@ All `bin/solr zk` sub-commands require a ZooKeeper connection string to function This means if you have `ZK_HOST` configured in your environment, you do not need to pass the `-z` option to every command. +=== ZooKeeper Subcommands + +All `bin/solr zk` subcommands have dedicated reference pages with full option documentation: + +[cols="1,3",options="header"] +|=== +|Command |Description + +|xref:cli/solr-zk-upconfig.adoc[upconfig] |Upload a configuration set to ZooKeeper +|xref:cli/solr-zk-downconfig.adoc[downconfig] |Download a configuration set from ZooKeeper +|xref:cli/solr-zk-cp.adoc[cp] |Copy files between the local file system and ZooKeeper, or between two ZooKeeper paths +|xref:cli/solr-zk-ls.adoc[ls] |List the contents of a ZooKeeper node +|xref:cli/solr-zk-rm.adoc[rm] |Remove a ZooKeeper path +|xref:cli/solr-zk-mv.adoc[mv] |Move a ZooKeeper node from one path to another +|xref:cli/solr-zk-mkroot.adoc[mkroot] |Create a root ZooKeeper path +|xref:cli/solr-zk-updateacls.adoc[updateacls] |Update Access Control on a ZooKeeper path +|=== + === Upload a Configuration Set Use the `zk upconfig` command to upload one of the pre-configured configuration sets or a customized configuration set to ZooKeeper. From ecde8f10ee04e120bd6b6848563fa1c5684e2dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Fri, 15 May 2026 14:26:07 +0200 Subject: [PATCH 10/19] Fix precommit Update lockfiles and sha-files Better --host docs and example on StartCommand Tweak example in CreateTool --- solr/core/gradle.lockfile | 2 +- .../apache/solr/cli/ConfigSetDownloadTool.java | 1 + .../org/apache/solr/cli/ConfigSetUploadTool.java | 1 + .../org/apache/solr/cli/ConnectionOptions.java | 6 ++++-- .../src/java/org/apache/solr/cli/CreateTool.java | 3 ++- .../org/apache/solr/cli/CredentialsOptions.java | 4 +++- .../src/java/org/apache/solr/cli/DeleteTool.java | 1 + .../src/java/org/apache/solr/cli/SolrCLI.java | 3 ++- .../java/org/apache/solr/cli/StartCommand.java | 15 +++++++++++---- .../src/java/org/apache/solr/cli/StatusTool.java | 1 + .../java/org/apache/solr/cli/UpdateACLTool.java | 1 + .../src/java/org/apache/solr/cli/VersionTool.java | 1 + .../src/java/org/apache/solr/cli/ZkCpTool.java | 1 + .../java/org/apache/solr/cli/ZkMkrootTool.java | 1 + .../src/java/org/apache/solr/cli/ZkMvTool.java | 1 + .../src/java/org/apache/solr/cli/ZkRmTool.java | 1 + .../apache/solr/cli/ZkSubcommandsPicocliTest.java | 5 +++-- solr/licenses/bcprov-jdk18on-1.81.1.jar.sha1 | 1 + solr/licenses/bcprov-jdk18on-1.81.jar.sha1 | 1 - solr/licenses/bcutil-jdk18on-1.81.1.jar.sha1 | 1 + solr/licenses/bcutil-jdk18on-1.81.jar.sha1 | 1 - solr/modules/jwt-auth/gradle.lockfile | 4 ++-- .../deployment-guide/pages/cli/solr-create.adoc | 2 +- .../deployment-guide/pages/cli/solr-start.adoc | 10 +++++----- 24 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 solr/licenses/bcprov-jdk18on-1.81.1.jar.sha1 delete mode 100644 solr/licenses/bcprov-jdk18on-1.81.jar.sha1 create mode 100644 solr/licenses/bcutil-jdk18on-1.81.1.jar.sha1 delete mode 100644 solr/licenses/bcutil-jdk18on-1.81.jar.sha1 diff --git a/solr/core/gradle.lockfile b/solr/core/gradle.lockfile index 512297f3c9a9..1c284a00949a 100644 --- a/solr/core/gradle.lockfile +++ b/solr/core/gradle.lockfile @@ -189,4 +189,4 @@ org.slf4j:jcl-over-slf4j:2.0.17=apiHelper,jarValidation,runtimeClasspath,runtime org.slf4j:slf4j-api:2.0.17=apiHelper,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath org.xerial.snappy:snappy-java:1.1.10.8=jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath ua.net.nlp:morfologik-ukrainian-search:4.9.1=jarValidation,testRuntimeClasspath -empty=apiHelperTest,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestUnusedDeclared,permitTestUsedUndeclared,permitUsedUndeclared,signatures +empty=apiHelperTest,missingdoclet,permitAggregatorUse,permitTestAggregatorUse,permitTestUnusedDeclared,permitTestUsedUndeclared,permitUnusedDeclared,permitUsedUndeclared,signatures diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java index e58f09468d41..5f414e4c2fc4 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; /** Supports zk downconfig command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "downconfig", description = "Download a configset from ZooKeeper to the local filesystem.", diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java index c35e6d572574..b1c534bda45a 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java @@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory; /** Supports zk upconfig command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "upconfig", description = "Upload a configset from the local filesystem to ZooKeeper.", diff --git a/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java index b34d81f4c6a2..2fda9d9e8007 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java +++ b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java @@ -16,6 +16,8 @@ */ package org.apache.solr.cli; +import picocli.CommandLine; + /** * Picocli ArgGroup for mutually-exclusive Solr URL / ZooKeeper connection options. * @@ -23,13 +25,13 @@ * ensure the user provides at most one of {@code --solr-url} or {@code --zk-host}. */ class ConnectionOptions { - @picocli.CommandLine.Option( + @CommandLine.Option( names = {"-s", "--solr-url"}, description = "Base Solr URL, which can be used to determine the zk-host if that's not known.") String solrUrl; - @picocli.CommandLine.Option( + @CommandLine.Option( names = {"-z", "--zk-host"}, description = "Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to " diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java b/solr/core/src/java/org/apache/solr/cli/CreateTool.java index 7668a21215ce..a2f3d92f7724 100644 --- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java @@ -43,6 +43,7 @@ import org.apache.solr.core.ConfigSetService; /** Supports create command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "create", mixinStandardHelpOptions = true, @@ -55,7 +56,7 @@ }, footerHeading = "%nExamples:%n", footer = { - " # Create a collection in SolrCloud mode", + " # Create a collection or core", " bin/solr create -c myCollection", "", " # Create a collection with 2 shards and 2 replicas", diff --git a/solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java b/solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java index 53bc24ca8b34..882792fc4b66 100644 --- a/solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java +++ b/solr/core/src/java/org/apache/solr/cli/CredentialsOptions.java @@ -16,13 +16,15 @@ */ package org.apache.solr.cli; +import picocli.CommandLine; + /** * Picocli mixin providing the common {@code --credentials} option. * *

Use as {@code @CommandLine.Mixin CredentialsOptions credentialsOptions} in a command class. */ public class CredentialsOptions { - @picocli.CommandLine.Option( + @CommandLine.Option( names = {"-u", "--credentials"}, description = "Credentials in the format username:password. Example: --credentials solr:SolrRocks") diff --git a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java index 059caed81e13..18cb3f61b315 100644 --- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java @@ -38,6 +38,7 @@ import org.slf4j.LoggerFactory; /** Supports delete command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "delete", mixinStandardHelpOptions = true, diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index 2fdc812c202c..f6ccd7d33333 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -54,6 +54,7 @@ import org.slf4j.LoggerFactory; /** Command-line utility for working with Solr. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "solr", version = "Apache Solr version " + SolrVersion.LATEST_STRING, @@ -90,7 +91,7 @@ public class SolrCLI implements CLIO { public static void exit(int exitStatus) { try { System.exit(exitStatus); - } catch (java.lang.SecurityException secExc) { + } catch (SecurityException secExc) { if (exitStatus != 0) throw new RuntimeException("SolrCLI failed to exit with status " + exitStatus); } diff --git a/solr/core/src/java/org/apache/solr/cli/StartCommand.java b/solr/core/src/java/org/apache/solr/cli/StartCommand.java index 4d1f6f410c76..06a747b361f9 100644 --- a/solr/core/src/java/org/apache/solr/cli/StartCommand.java +++ b/solr/core/src/java/org/apache/solr/cli/StartCommand.java @@ -21,7 +21,7 @@ @CommandLine.Command( name = "start", - description = "Starts Solr in standalone or SolrCloud mode.", + description = "Starts Solr", footerHeading = "%nExamples:%n", footer = { " # Start Solr in SolrCloud mode on the default port", @@ -30,8 +30,8 @@ " # Start on a custom port with 2g heap", " bin/solr start -p 8984 -m 2g", "", - " # Start in standalone mode with an external ZooKeeper", - " bin/solr start --user-managed -z localhost:2181/solr" + " # Start in user managed mode", + " bin/solr start --user-managed" }) public class StartCommand implements Callable { @@ -50,7 +50,11 @@ public class StartCommand implements Callable { description = "Start Solr in standalone mode. Default is SolrCloud (ZooKeeper) mode.") boolean userManaged; - @CommandLine.Option(names = "--host", description = "Specify the hostname for this Solr instance") + @CommandLine.Option( + names = "--host", + description = + "Specify the hostname this Solr node will advertise to ZooKeeper and other cluster members (SOLR_HOST_ADVERTISE)." + + " Defaults to the canonical hostname of the local host.") String host; @CommandLine.Option( @@ -136,6 +140,8 @@ public class StartCommand implements Callable { description = "Set log level to WARN (quiet); default is INFO") boolean quiet; + // TODO: SOLR-17697 remove --fullhelp when commons-cli is removed; picocli's --help already + // produces equivalent output from the annotations. @CommandLine.Option( names = "--fullhelp", description = "Print detailed help with full option descriptions", @@ -151,6 +157,7 @@ public Integer call() { return 0; } + // TODO: Remove when removing commons-cli private void printFullHelp() { CLIO.out( """ diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index 6a9b8b0fe079..b5118ca2e3c1 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -44,6 +44,7 @@ * *

Get the status of a Solr server. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "status", description = "Get the status of a Solr server.", diff --git a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java index a7a8dee64038..4a1e6cf331d6 100644 --- a/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java +++ b/solr/core/src/java/org/apache/solr/cli/UpdateACLTool.java @@ -29,6 +29,7 @@ * *

Set ACL properties by directly manipulating ZooKeeper. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "updateacls", description = diff --git a/solr/core/src/java/org/apache/solr/cli/VersionTool.java b/solr/core/src/java/org/apache/solr/cli/VersionTool.java index f8f8393dbde5..121fc1544751 100644 --- a/solr/core/src/java/org/apache/solr/cli/VersionTool.java +++ b/solr/core/src/java/org/apache/solr/cli/VersionTool.java @@ -20,6 +20,7 @@ import org.apache.commons.cli.CommandLine; import org.apache.solr.client.api.util.SolrVersion; +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command(name = "version", description = "Prints the Solr version.") public class VersionTool extends ToolBase { diff --git a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java index ffdce4ef6a5f..7b7ed5778fe8 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkCpTool.java @@ -39,6 +39,7 @@ import org.slf4j.LoggerFactory; /** Supports zk cp command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "cp", description = { diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java index eec379b1d565..d52a3d90cb5a 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMkrootTool.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; /** Supports zk mkroot command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "mkroot", description = diff --git a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java index ca9c1f50a5aa..8a0db12b037a 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkMvTool.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; /** Supports zk mv command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "mv", description = { diff --git a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java index cf700a8b8fd3..9b9bbb23a58e 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkRmTool.java @@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory; /** Supports zk rm command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "rm", description = "Remove a znode from ZooKeeper.", diff --git a/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsPicocliTest.java b/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsPicocliTest.java index 39b593d7dc72..9b631013cd56 100644 --- a/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsPicocliTest.java +++ b/solr/core/src/test/org/apache/solr/cli/ZkSubcommandsPicocliTest.java @@ -17,6 +17,7 @@ package org.apache.solr.cli; import java.util.Arrays; +import picocli.CommandLine; /** * Runs all {@link ZkSubcommandsTest} tests through the picocli invocation path. @@ -32,7 +33,7 @@ protected int runTool(String[] args, Class clazz) throws Exc // args[0] is the tool/subcommand name used by commons-cli dispatch; strip it for picocli. String[] toolArgs = Arrays.copyOfRange(args, 1, args.length); ToolBase tool = clazz.getDeclaredConstructor().newInstance(); - return new picocli.CommandLine(tool) + return new CommandLine(tool) .setDefaultValueProvider(new CliDefaultValueProvider()) .execute(toolArgs); } @@ -43,7 +44,7 @@ protected int runTool( throws Exception { String[] toolArgs = Arrays.copyOfRange(args, 1, args.length); ToolBase tool = clazz.getDeclaredConstructor(ToolRuntime.class).newInstance(runtime); - return new picocli.CommandLine(tool) + return new CommandLine(tool) .setDefaultValueProvider(new CliDefaultValueProvider()) .execute(toolArgs); } diff --git a/solr/licenses/bcprov-jdk18on-1.81.1.jar.sha1 b/solr/licenses/bcprov-jdk18on-1.81.1.jar.sha1 new file mode 100644 index 000000000000..74289a6ddbfd --- /dev/null +++ b/solr/licenses/bcprov-jdk18on-1.81.1.jar.sha1 @@ -0,0 +1 @@ +07bcb4c4664600c51190ab3774710a51bfdeb2eb diff --git a/solr/licenses/bcprov-jdk18on-1.81.jar.sha1 b/solr/licenses/bcprov-jdk18on-1.81.jar.sha1 deleted file mode 100644 index e600ba115854..000000000000 --- a/solr/licenses/bcprov-jdk18on-1.81.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -d17c094daef57dbd80af71687a475aa6df7cbe54 diff --git a/solr/licenses/bcutil-jdk18on-1.81.1.jar.sha1 b/solr/licenses/bcutil-jdk18on-1.81.1.jar.sha1 new file mode 100644 index 000000000000..d53e262af267 --- /dev/null +++ b/solr/licenses/bcutil-jdk18on-1.81.1.jar.sha1 @@ -0,0 +1 @@ +cd685b869ed03e8557674f00969e66e0e2ccf3aa diff --git a/solr/licenses/bcutil-jdk18on-1.81.jar.sha1 b/solr/licenses/bcutil-jdk18on-1.81.jar.sha1 deleted file mode 100644 index 61b2863549d3..000000000000 --- a/solr/licenses/bcutil-jdk18on-1.81.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -e2dd79395ab435094142b6aba219f35adcba0f01 diff --git a/solr/modules/jwt-auth/gradle.lockfile b/solr/modules/jwt-auth/gradle.lockfile index 010d84b4250a..e002475d56a2 100644 --- a/solr/modules/jwt-auth/gradle.lockfile +++ b/solr/modules/jwt-auth/gradle.lockfile @@ -139,9 +139,9 @@ org.bitbucket.b_c:jose4j:0.9.6=compileClasspath,jarValidation,runtimeClasspath,r org.bouncycastle:bcpkix-jdk15on:1.70=jarValidation,testRuntimeClasspath org.bouncycastle:bcpkix-jdk18on:1.81=jarValidation,testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcprov-jdk15on:1.70=jarValidation,testRuntimeClasspath -org.bouncycastle:bcprov-jdk18on:1.81=jarValidation,testCompileClasspath,testRuntimeClasspath +org.bouncycastle:bcprov-jdk18on:1.81.1=jarValidation,testCompileClasspath,testRuntimeClasspath org.bouncycastle:bcutil-jdk15on:1.70=jarValidation,testRuntimeClasspath -org.bouncycastle:bcutil-jdk18on:1.81=jarValidation,testCompileClasspath,testRuntimeClasspath +org.bouncycastle:bcutil-jdk18on:1.81.1=jarValidation,testCompileClasspath,testRuntimeClasspath org.codehaus.woodstox:stax2-api:4.2.2=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.27=jarValidation,testCompileClasspath,testRuntimeClasspath org.eclipse.jetty.http2:jetty-http2-client-transport:12.0.27=jarValidation,runtimeClasspath,runtimeLibs,solrPlatformLibs,testRuntimeClasspath diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc index 47112c051273..24ba2f3903af 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc @@ -106,7 +106,7 @@ Creates a core or collection depending on whether Solr is running in standalone == Examples: [%hardbreaks] - # Create a collection in SolrCloud mode + # Create a collection or core bin/solr create -c myCollection [%hardbreaks] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-start.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-start.adoc index 15752e38cfea..c06f225e7a07 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-start.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-start.adoc @@ -25,7 +25,7 @@ // tag::picocli-generated-man-section-name[] == Name -bin/solr start - Starts Solr in standalone or SolrCloud mode. +bin/solr start - Starts Solr // end::picocli-generated-man-section-name[] @@ -45,7 +45,7 @@ bin/solr start [-fhqy] [--force] [--user-managed] [--verbose] // tag::picocli-generated-man-section-description[] == Description -Starts Solr in standalone or SolrCloud mode. +Starts Solr // end::picocli-generated-man-section-description[] @@ -71,7 +71,7 @@ Starts Solr in standalone or SolrCloud mode. Print this help message and exit. *--host*=__:: - Specify the hostname for this Solr instance + Specify the hostname this Solr node will advertise to ZooKeeper and other cluster members (SOLR_HOST_ADVERTISE). Defaults to the canonical hostname of the local host. *-j*, *--jettyconfig*=__:: Additional Jetty parameters, e.g., -j "--include-jetty-dir=/etc/jetty/custom/server/" @@ -132,7 +132,7 @@ Starts Solr in standalone or SolrCloud mode. bin/solr start -p 8984 -m 2g [%hardbreaks] - # Start in standalone mode with an external ZooKeeper - bin/solr start --user-managed -z localhost:2181/solr + # Start in user managed mode + bin/solr start --user-managed // end::picocli-generated-man-section-footer[] From d113b0f29af65ee3552cb6c58890baadc1a86bfb Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Mon, 18 May 2026 10:37:45 -0400 Subject: [PATCH 11/19] SOLR-17697: visually flag required options (#4433) --- solr/solr-ref-guide/build.gradle | 63 +++++++++++++++++++ .../pages/cli/solr-create.adoc | 2 +- .../pages/cli/solr-delete.adoc | 2 +- .../pages/cli/solr-zk-cp.adoc | 4 +- .../pages/cli/solr-zk-downconfig.adoc | 4 +- .../pages/cli/solr-zk-ls.adoc | 2 +- .../pages/cli/solr-zk-mkroot.adoc | 2 +- .../pages/cli/solr-zk-mv.adoc | 4 +- .../pages/cli/solr-zk-rm.adoc | 2 +- .../pages/cli/solr-zk-upconfig.adoc | 4 +- .../pages/cli/solr-zk-updateacls.adoc | 2 +- 11 files changed, 77 insertions(+), 14 deletions(-) diff --git a/solr/solr-ref-guide/build.gradle b/solr/solr-ref-guide/build.gradle index 5b9dc95bd89f..968ac6d711df 100644 --- a/solr/solr-ref-guide/build.gradle +++ b/solr/solr-ref-guide/build.gradle @@ -592,6 +592,67 @@ String cliFileNameToNavLabel(String baseName) { return baseName.split('-').last() } +// Strip "[...]" (optional groups) and "(...)" (required groups whose members +// aren't *individually* required), repeating until no inner groups remain. +// Tokens left in the result are the ones the user must always provide. +String stripSynopsisGroups(String text) { + String s = text + String prev = null + while (s != prev) { + prev = s + s = s.replaceAll(/\[[^\[\]]*\]/, '') + s = s.replaceAll(/\([^()]*\)/, '') + } + return s +} + +Set extractRequiredTokens(String synopsis, java.util.regex.Pattern pattern) { + def result = new LinkedHashSet() + def m = stripSynopsisGroups(synopsis) =~ pattern + while (m.find()) { result.add(m.group(1)) } + return result +} + +// Parse the wrapped synopsis to find required (unbracketed) options and positional +// args, then prepend a "*(required)*" marker to their description lines in the +// Options and Arguments sections. Picocli's ManPageGenerator doesn't emit a +// per-option required marker, so we use the synopsis bracketing as the source of +// truth instead of reaching into the CommandSpec. +// See https://github.com/remkop/picocli/issues/2519. +String markRequiredOptionsAndArgs(String content) { + final String MARKER = '*(required)*' + final def OPTION_NAME = ~/(-{1,2}[a-zA-Z][a-zA-Z0-9-]*)/ + final def POSITIONAL_NAME = ~/(<[^<>\s]+>)/ + + def synopsisMatch = content =~ /(?s)== Synopsis\n\n\.{4}\n(.*?)\n\.{4}/ + if (!synopsisMatch.find()) return content + + def synopsisText = synopsisMatch.group(1).replaceAll(/\s+/, ' ') + def requiredOpts = extractRequiredTokens(synopsisText, OPTION_NAME) + def requiredArgs = extractRequiredTokens(synopsisText, POSITIONAL_NAME) + + if (!requiredOpts.isEmpty()) { + content = content.replaceAll(/(?m)^(\*-[^\n]+::)\n( +)([^\n]+)/) { List m -> + def (label, indent, descLine) = [m[1], m[2], m[3]] + if (descLine.startsWith(MARKER)) return m[0] + def beforeEq = label.replaceAll(/[*_]/, '').split('=', 2)[0] + def names = (beforeEq =~ OPTION_NAME).collect { it[1] } + names.any { requiredOpts.contains(it) } + ? "${label}\n${indent}${MARKER} ${descLine}" : m[0] + } + } + + if (!requiredArgs.isEmpty()) { + content = content.replaceAll(/(?m)^_(<[^<>\n]+>)_::\n( +)([^\n]+)/) { List m -> + def (name, indent, descLine) = [m[1], m[2], m[3]] + if (descLine.startsWith(MARKER)) return m[0] + requiredArgs.contains(name) + ? "_${name}_::\n${indent}${MARKER} ${descLine}" : m[0] + } + } + return content +} + // Post-process a raw ManPageGenerator AsciiDoc file for Antora compatibility. // The title lives inside the man-section-header block, so we replace the whole // header block with an Antora-compatible page title + attributes. @@ -631,6 +692,8 @@ String postProcessCliManPage(File rawFile, String asfHeader, String doNotEditNot "${m[1]}....\n${synopsis}\n....${m[3]}" } + content = markRequiredOptionsAndArgs(content) + // Strip the outer full-manpage tag wrappers content = content.replace("// tag::picocli-generated-full-manpage[]\n", '') content = content.replace("// end::picocli-generated-full-manpage[]\n", '') diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc index 24ba2f3903af..74417d2a1943 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc @@ -51,7 +51,7 @@ Creates a core or collection depending on whether Solr is running in standalone == Options *-c*, *--name*=__:: - Name of collection or core to create. + *(required)* Name of collection or core to create. *-d*, *--conf-dir*=__:: Configuration directory to copy when creating the new collection; default is _default. diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc index 1f8a50e52797..a7601a9d606d 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc @@ -50,7 +50,7 @@ Deletes a collection or core depending on whether Solr is running in SolrCloud o == Options *-c*, *--name*=__:: - Name of the core / collection to delete. + *(required)* Name of the core / collection to delete. *--delete-config*:: Flag to indicate if the underlying configuration directory for a collection should also be deleted; default is false. diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc index 38c55fec065a..39fdfc642fbb 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc @@ -79,10 +79,10 @@ and the last element of the path will be appended unless also ends i == Arguments __:: - Source path: [file:][/]path/to/local/file or zk:/path/to/zk/node. + *(required)* Source path: [file:][/]path/to/local/file or zk:/path/to/zk/node. __:: - Destination path: [file:][/]path/to/local/file or zk:/path/to/zk/node. + *(required)* Destination path: [file:][/]path/to/local/file or zk:/path/to/zk/node. // end::picocli-generated-man-section-arguments[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc index 65c2b5de424d..980cc0de0368 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc @@ -50,10 +50,10 @@ Download a configset from ZooKeeper to the local filesystem. == Options *-d*, *--conf-dir*=_DIR_:: - Local directory with configs. + *(required)* Local directory with configs. *-n*, *--conf-name*=__:: - Configset name in ZooKeeper. + *(required)* Configset name in ZooKeeper. *-s*, *--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc index 4c5d82be3d6a..fe3588d5abda 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc @@ -69,7 +69,7 @@ List the contents of a ZooKeeper node. == Arguments __:: - The path of the ZooKeeper znode path to list. + *(required)* The path of the ZooKeeper znode path to list. // end::picocli-generated-man-section-arguments[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc index 5df27141a8de..79582b93e27a 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc @@ -70,7 +70,7 @@ Make a znode in ZooKeeper with no data. Can be used to make a path of arbitrary == Arguments __:: - The ZooKeeper znode path to create. + *(required)* The ZooKeeper znode path to create. // end::picocli-generated-man-section-arguments[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc index 46279f49fd95..1a86d32719aa 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc @@ -70,10 +70,10 @@ and the last element of the path will be appended. == Arguments __:: - Source ZooKeeper znode path (zk: prefix optional). + *(required)* Source ZooKeeper znode path (zk: prefix optional). __:: - Destination ZooKeeper znode path (zk: prefix optional). + *(required)* Destination ZooKeeper znode path (zk: prefix optional). // end::picocli-generated-man-section-arguments[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc index c752f4100360..b194510bbaec 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc @@ -69,7 +69,7 @@ Remove a znode from ZooKeeper. == Arguments __:: - The ZooKeeper znode path to remove (zk: prefix optional). + *(required)* The ZooKeeper znode path to remove (zk: prefix optional). // end::picocli-generated-man-section-arguments[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc index 0b796355c9a0..e6a04d7063e3 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc @@ -50,10 +50,10 @@ Upload a configset from the local filesystem to ZooKeeper. == Options *-d*, *--conf-dir*=_DIR_:: - Local directory with configs. + *(required)* Local directory with configs. *-n*, *--conf-name*=__:: - Configset name in ZooKeeper. + *(required)* Configset name in ZooKeeper. *-s*, *--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc index e85f005325f0..da0c05f74370 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc @@ -66,7 +66,7 @@ Recursively re-applies ZooKeeper ACLs to a znode and all its descendants. The AC == Arguments __:: - The ZooKeeper znode path to update ACLs for. + *(required)* The ZooKeeper znode path to update ACLs for. // end::picocli-generated-man-section-arguments[] From b064eb6c8170d05948df012e56a10041811c0695 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sun, 21 Jun 2026 07:53:52 -0400 Subject: [PATCH 12/19] Spike of supporting longer option description for ref guide and short for cli (#4443) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../solr/cli/ConfigSetDownloadTool.java | 19 ++++++++++- .../org/apache/solr/cli/ConfigSetOptions.java | 19 ++++++----- .../apache/solr/cli/ConfigSetUploadTool.java | 19 ++++++++++- .../src/java/org/apache/solr/cli/SolrCLI.java | 26 ++++++++++++++ .../test/org/apache/solr/cli/SolrCLITest.java | 23 +++++++++++++ .../pages/cli/solr-zk-downconfig.adoc | 34 ++++++++++++++++++- .../pages/cli/solr-zk-upconfig.adoc | 34 ++++++++++++++++++- 7 files changed, 162 insertions(+), 12 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java index 5f414e4c2fc4..33d7727f6e2f 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetDownloadTool.java @@ -66,6 +66,23 @@ public class ConfigSetDownloadTool extends ToolBase { @picocli.CommandLine.Mixin ConfigSetOptions configSetOpts; + @picocli.CommandLine.Option( + names = {"-d", "--conf-dir"}, + description = + """ + Local directory for configs. + The path to write the downloaded configuration set into. If just a name is supplied, `$SOLR_TIP/server/solr/configsets` will be the parent. An absolute path may be supplied as well. + + In either case, _pre-existing configurations at the destination will be overwritten_! + + **Examples:** + * `-d directory_under_configsets` + * `-d /path/to/configset/destination` + """, + required = true, + paramLabel = "DIR") + public String confDir; + public ConfigSetDownloadTool() { this(new DefaultToolRuntime()); } @@ -138,7 +155,7 @@ public int callTool() throws Exception { .withUrl(zkHost) .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) .build()) { - doDownconfig(zkClient, zkHost, configSetOpts.confName, configSetOpts.confDir); + doDownconfig(zkClient, zkHost, configSetOpts.confName, confDir); return 0; } catch (Exception e) { log.error("Could not complete downconfig operation for reason: ", e); diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetOptions.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetOptions.java index 61eb657ff776..c5b2b1a77ad0 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetOptions.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetOptions.java @@ -28,14 +28,17 @@ public class ConfigSetOptions { @CommandLine.Option( names = {"-n", "--conf-name"}, - description = "Configset name in ZooKeeper.", + description = + """ + Configset name in ZooKeeper. + Name of the configuration set under the "/configs" ZooKeeper node. + + You can see available configuration sets in the Admin UI via the Cloud screens. Choose Cloud → Tree → configs to see them. + + If a pre-existing configuration set is specified, it will be overwritten in ZooKeeper. + + **Example:** `-n myconfig` + """, required = true) public String confName; - - @CommandLine.Option( - names = {"-d", "--conf-dir"}, - description = "Local directory with configs.", - required = true, - paramLabel = "DIR") - public String confDir; } diff --git a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java index b1c534bda45a..69b902064f41 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java +++ b/solr/core/src/java/org/apache/solr/cli/ConfigSetUploadTool.java @@ -68,6 +68,23 @@ public class ConfigSetUploadTool extends ToolBase { @picocli.CommandLine.Mixin ConfigSetOptions configSetOpts; + @picocli.CommandLine.Option( + names = {"-d", "--conf-dir"}, + description = + """ + Local directory for configs. + The local directory of the configuration set to upload. It should have a `conf` directory immediately below it that in turn contains `solrconfig.xml` etc. + + If just a name is supplied, `$SOLR_TIP/server/solr/configsets` will be checked for this name. An absolute path may be supplied instead. + + **Examples:** + * `-d directory_under_configsets` + * `-d /path/to/configset/source` + """, + required = true, + paramLabel = "DIR") + public String confDir; + public ConfigSetUploadTool() { this(new DefaultToolRuntime()); } @@ -142,7 +159,7 @@ public int callTool() throws Exception { .withUrl(zkHost) .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) .build()) { - doUpconfig(zkClient, zkHost, configSetOpts.confName, configSetOpts.confDir); + doUpconfig(zkClient, zkHost, configSetOpts.confName, confDir); return 0; } catch (Exception e) { log.error("Could not complete upconfig operation for reason: ", e); diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index f6ccd7d33333..71ae5ec8f48b 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -109,8 +109,34 @@ public static void main(String[] args) throws Exception { } } + /** + * Truncates each option's description to its first line for interactive {@code --help} output. + * The remaining lines of the {@code description} array are reserved for the generated ref-guide. + * This customization is only installed on the interactive CLI command setup, so ref-guide + * generation, which runs the doc generator directly, still sees the full description. + */ + @VisibleForTesting + static void installFirstLineOnlyHelpFactory(picocli.CommandLine cmd) { + cmd.setHelpFactory( + (spec, colorScheme) -> + new picocli.CommandLine.Help(spec, colorScheme) { + @Override + public picocli.CommandLine.Help.IOptionRenderer createDefaultOptionRenderer() { + picocli.CommandLine.Help.IOptionRenderer base = super.createDefaultOptionRenderer(); + return (option, paramLabelRenderer, scheme) -> { + picocli.CommandLine.Help.Ansi.Text[][] rows = + base.render(option, paramLabelRenderer, scheme); + return rows.length <= 1 + ? rows + : new picocli.CommandLine.Help.Ansi.Text[][] {rows[0]}; + }; + } + }); + } + /** Propagates common settings to all subcommands. */ private static void propagateCommandSettings(picocli.CommandLine cmd) { + installFirstLineOnlyHelpFactory(cmd); for (picocli.CommandLine subcommand : cmd.getSubcommands().values()) { subcommand.getCommandSpec().defaultValueProvider(cmd.getCommandSpec().defaultValueProvider()); subcommand.getCommandSpec().usageMessage().width(cmd.getCommandSpec().usageMessage().width()); diff --git a/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java b/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java index d045ae35501e..50cfcfed3cd0 100644 --- a/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java +++ b/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java @@ -36,4 +36,27 @@ public void testUptime() { assertEquals( "106751991167 days, 7 hours, 12 minutes, 56 seconds", SolrCLI.uptime(Long.MAX_VALUE)); } + + @Test + public void testFirstLineOnlyHelpFactoryTruncatesMultiLineDescription() { + picocli.CommandLine cmd = new picocli.CommandLine(new MultiLineDescriptionCommand()); + + String baseline = cmd.getUsageMessage(); + assertTrue("baseline should include first line", baseline.contains("First line.")); + assertTrue("baseline should include second line", baseline.contains("Second line.")); + + SolrCLI.installFirstLineOnlyHelpFactory(cmd); + String truncated = cmd.getUsageMessage(); + assertTrue("truncated should include first line", truncated.contains("First line.")); + assertFalse("truncated should NOT include second line", truncated.contains("Second line.")); + assertFalse("truncated should NOT include third line", truncated.contains("Third line.")); + } + + @picocli.CommandLine.Command(name = "test") + private static class MultiLineDescriptionCommand { + @picocli.CommandLine.Option( + names = "--multi", + description = {"First line.", "Second line.", "Third line."}) + String multi; + } } diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc index 980cc0de0368..342566fcf6a6 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc @@ -50,10 +50,42 @@ Download a configset from ZooKeeper to the local filesystem. == Options *-d*, *--conf-dir*=_DIR_:: - *(required)* Local directory with configs. + *(required)* Local directory for configs. ++ +The path to write the downloaded configuration set into. If just a name is supplied, `$SOLR_TIP/server/solr/configsets` will be the parent. An absolute path may be supplied as well. ++ + ++ +In either case, _pre-existing configurations at the destination will be overwritten_! ++ + ++ +**Examples:** ++ +* `-d directory_under_configsets` ++ +* `-d /path/to/configset/destination` ++ + *-n*, *--conf-name*=__:: *(required)* Configset name in ZooKeeper. ++ +Name of the configuration set under the "/configs" ZooKeeper node. ++ + ++ +You can see available configuration sets in the Admin UI via the Cloud screens. Choose Cloud → Tree → configs to see them. ++ + ++ +If a pre-existing configuration set is specified, it will be overwritten in ZooKeeper. ++ + ++ +**Example:** `-n myconfig` ++ + *-s*, *--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc index e6a04d7063e3..7353719867ff 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc @@ -50,10 +50,42 @@ Upload a configset from the local filesystem to ZooKeeper. == Options *-d*, *--conf-dir*=_DIR_:: - *(required)* Local directory with configs. + *(required)* Local directory for configs. ++ +The local directory of the configuration set to upload. It should have a `conf` directory immediately below it that in turn contains `solrconfig.xml` etc. ++ + ++ +If just a name is supplied, `$SOLR_TIP/server/solr/configsets` will be checked for this name. An absolute path may be supplied instead. ++ + ++ +**Examples:** ++ +* `-d directory_under_configsets` ++ +* `-d /path/to/configset/source` ++ + *-n*, *--conf-name*=__:: *(required)* Configset name in ZooKeeper. ++ +Name of the configuration set under the "/configs" ZooKeeper node. ++ + ++ +You can see available configuration sets in the Admin UI via the Cloud screens. Choose Cloud → Tree → configs to see them. ++ + ++ +If a pre-existing configuration set is specified, it will be overwritten in ZooKeeper. ++ + ++ +**Example:** `-n myconfig` ++ + *-s*, *--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known From 1b51884b7c325e2a461407b3fa0202b95f704a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 30 Jul 2026 15:22:12 +0200 Subject: [PATCH 13/19] Update lockfiles for jar-checks Copy configurations after merging main --- solr/core/gradle.lockfile | 2 +- solr/solrj-zookeeper/gradle.lockfile | 2 +- solr/test-framework/gradle.lockfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/solr/core/gradle.lockfile b/solr/core/gradle.lockfile index 9e9a71a1851b..768175ab0624 100644 --- a/solr/core/gradle.lockfile +++ b/solr/core/gradle.lockfile @@ -40,7 +40,7 @@ commons-cli:commons-cli:1.11.0=cliDocsRuntime,compileClasspath,compileClasspathC commons-codec:commons-codec:1.22.0=cliDocsRuntime,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy commons-io:commons-io:2.22.0=cliDocsRuntime,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy info.picocli:picocli-codegen:4.7.6=annotationProcessor,cliDocsRuntime -info.picocli:picocli:4.7.6=annotationProcessor,cliDocsRuntime,compileClasspath,jarValidation,runtimeClasspath,runtimeLibs,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=annotationProcessor,cliDocsRuntime,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.dropwizard.metrics:metrics-core:4.2.39=cliDocsRuntime,jarValidation,runtimeClasspath,runtimeClasspathCopy,runtimeLibs,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor diff --git a/solr/solrj-zookeeper/gradle.lockfile b/solr/solrj-zookeeper/gradle.lockfile index 78dd0f3c271e..d03893eef99f 100644 --- a/solr/solrj-zookeeper/gradle.lockfile +++ b/solr/solrj-zookeeper/gradle.lockfile @@ -35,7 +35,7 @@ com.tdunning:t-digest:3.3=jarValidation,testRuntimeClasspath,testRuntimeClasspat commons-cli:commons-cli:1.11.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy commons-codec:commons-codec:1.22.0=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy commons-io:commons-io:2.22.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy -info.picocli:picocli:4.7.6=jarValidation,testRuntimeClasspath +info.picocli:picocli:4.7.6=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy io.dropwizard.metrics:metrics-core:4.2.39=jarValidation,testRuntimeClasspath,testRuntimeClasspathCopy io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor diff --git a/solr/test-framework/gradle.lockfile b/solr/test-framework/gradle.lockfile index 1bec5bce7433..9f76cc98d1c9 100644 --- a/solr/test-framework/gradle.lockfile +++ b/solr/test-framework/gradle.lockfile @@ -37,7 +37,7 @@ commons-cli:commons-cli:1.11.0=compileClasspath,compileClasspathCopy,jarValidati commons-codec:commons-codec:1.22.0=jarValidation,runtimeClasspath,runtimeClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy commons-io:commons-io:2.22.0=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy info.picocli:picocli-codegen:4.7.6=annotationProcessor -info.picocli:picocli:4.7.6=annotationProcessor,compileClasspath,jarValidation,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +info.picocli:picocli:4.7.6=annotationProcessor,compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.dropwizard.metrics:metrics-core:4.2.39=compileClasspath,compileClasspathCopy,jarValidation,runtimeClasspath,runtimeClasspathCopy,testCompileClasspath,testCompileClasspathCopy,testRuntimeClasspath,testRuntimeClasspathCopy io.github.eisop:dataflow-errorprone:3.41.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor From b990f9f6eb18617bcb97733d0a91a8a4039afd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 30 Jul 2026 15:30:54 +0200 Subject: [PATCH 14/19] Post-merge review fixes: align DeleteTool/CreateTool with main's ConfigSets API changes - DeleteTool: use CLIUtils.getSolrConnection and 'Connecting to Solr at' message as on main, since delete no longer talks to ZooKeeper directly - CreateTool: only take solrUrl from explicit connection options so the live-node URL fallback works again (as on main) - StatusTool: default picocli --max-wait-secs to 0 to avoid NPE --- .../src/java/org/apache/solr/cli/CreateTool.java | 5 +++-- .../src/java/org/apache/solr/cli/DeleteTool.java | 13 +++++++++---- .../src/java/org/apache/solr/cli/StatusTool.java | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java b/solr/core/src/java/org/apache/solr/cli/CreateTool.java index 22debb00719e..1538e634a7ae 100644 --- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java @@ -206,7 +206,7 @@ public void runImpl(CommandLine cli) throws Exception { cli.getOptionValue(COLLECTION_NAME_OPTION), cli.getOptionValue(CONF_DIR_OPTION, DefaultValues.DEFAULT_CONFIG_SET), cli.getOptionValue(CONF_NAME_OPTION), - cli.getOptionValue(CommonCLIOptions.SOLR_URL_OPTION, CLIUtils.getDefaultSolrUrl()), + CLIUtils.hasConnectionOption(cli) ? CLIUtils.normalizeSolrUrl(cli) : null, cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), cli.getParsedOptionValue(SHARDS_OPTION, 1), cli.getParsedOptionValue(REPLICATION_FACTOR_OPTION, 1)); @@ -232,7 +232,8 @@ private void createCore(CreateParams params, SolrClient solrClient) throws Excep // usually same as solr home, but not always String coreRootDirectory = sysResponse.getCoreRoot(); - if (CLIUtils.safeCheckCoreExists(params.solrUrl, params.name, params.credentials)) { + String solrUrl = params.solrUrl != null ? params.solrUrl : CLIUtils.getDefaultSolrUrl(); + if (CLIUtils.safeCheckCoreExists(solrUrl, params.name, params.credentials)) { throw new IllegalArgumentException( "\nCore '" + params.name diff --git a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java index 2bacd58e6dec..a17b351b0a26 100644 --- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java @@ -153,23 +153,28 @@ public void runImpl(CommandLine cli) throws Exception { cli.hasOption(DELETE_CONFIG_OPTION), cli.hasOption(FORCE_OPTION)); if (CLIUtils.isCloudMode(solrClient)) { - deleteCollection(CLIUtils.getZkHost(cli), params); + deleteCollection(CLIUtils.getSolrConnection(cli), params); } else { deleteCore(params, solrClient); } } } - private void deleteCollection(String zkHost, DeleteParams params) throws Exception { + private void deleteCollection(String connectionString, DeleteParams params) throws Exception { + deleteCollection(CloudSolrClient.CloudSolrClientConnection.parse(connectionString), params); + } + + private void deleteCollection( + CloudSolrClient.CloudSolrClientConnection solrConnection, DeleteParams params) + throws Exception { var builder = new HttpJettySolrClient.Builder() .withIdleTimeout(30, TimeUnit.SECONDS) .withConnectionTimeout(15, TimeUnit.SECONDS) .withKeyStoreReloadInterval(-1, TimeUnit.SECONDS) .withOptionalBasicAuthCredentials(params.credentials); - echoIfVerbose("Connecting to ZooKeeper at " + zkHost); - var solrConnection = CloudSolrClient.CloudSolrClientConnection.parse(zkHost); try (var cloudSolrClient = CLIUtils.getCloudSolrClient(solrConnection, builder)) { + echoIfVerbose("Connecting to Solr at " + solrConnection.toString()); deleteCollection(cloudSolrClient, params); } } diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index 4b6a0be298af..01783a8de5a9 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -67,6 +67,7 @@ public class StatusTool extends ToolBase { @picocli.CommandLine.Option( names = {"--max-wait-secs"}, + defaultValue = "0", description = "Wait up to the specified number of seconds to see Solr running.") private Integer maxWaitSecs; From 6f680d32aec4f11e808b18a8b57485c4772a46e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 30 Jul 2026 16:07:26 +0200 Subject: [PATCH 15/19] SOLR-17697: Bring picocli path to parity with main's --solr-connection changes - SolrCLI --version now prints 'Client version:' matching commons-cli output - ZkConnectionOptions, ConnectionOptions (create/delete) and StatusTool gain -s/--solr-connection accepting a ZooKeeper or HTTP(s) connection string, with --solr-url now long-only, mirroring CommonCLIOptions on main - StatusTool picocli target options grouped mutually exclusive like the commons-cli OptionGroup; zk/connection targets resolved to a Solr URL via new shared CLIUtils.solrUrlFromConnection (extracted from normalizeSolrUrl) - CliDefaultValueProvider supports SOLR_CONNECTION env default - test_status.bats accepts both engines' mutual-exclusion error messages Connection parsing is string-only; network calls happen only at tool execution time, never during picocli parsing. --- .../java/org/apache/solr/cli/CLIUtils.java | 50 ++++++++++------- .../solr/cli/CliDefaultValueProvider.java | 1 + .../apache/solr/cli/ConnectionOptions.java | 38 ++++++++++++- .../java/org/apache/solr/cli/CreateTool.java | 6 +- .../java/org/apache/solr/cli/DeleteTool.java | 6 +- .../src/java/org/apache/solr/cli/SolrCLI.java | 2 +- .../java/org/apache/solr/cli/StatusTool.java | 55 +++++++++++++++++-- .../apache/solr/cli/ZkConnectionOptions.java | 27 ++++++++- solr/packaging/test/test_status.bats | 2 +- 9 files changed, 150 insertions(+), 37 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java index 07fc8c89f745..0aa1941ce795 100644 --- a/solr/core/src/java/org/apache/solr/cli/CLIUtils.java +++ b/solr/core/src/java/org/apache/solr/cli/CLIUtils.java @@ -234,34 +234,42 @@ public static String normalizeSolrUrl(CommandLine cli) throws Exception { "Neither --solr-connection, --zk-host or --solr-url parameters, nor SOLR_CONNECTION, ZK_HOST env var provided, so assuming solr url is " + solrUrl + "."); - } else if (!solrConnection.isZookeeper()) { - // HTTP form (e.g. `-s http://host:port`): the connection string already names a Solr URL, - // so use it directly without spinning up a CloudSolrClient. - solrUrl = normalizeSolrUrl(solrConnection.quorumItems().get(0), false); } else { - var builder = - new HttpJettySolrClient.Builder() - .withOptionalBasicAuthCredentials( - cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION)); - try (CloudSolrClient cloudSolrClient = getCloudSolrClient(solrConnection, builder)) { - Set liveNodes = cloudSolrClient.getClusterState().getLiveNodes(); - if (liveNodes.isEmpty()) - throw new IllegalStateException( - "No live nodes found! Cannot determine 'solrUrl' from SolrCloud: " - + solrConnection); - - String firstLiveNode = liveNodes.iterator().next(); - String urlScheme = - cloudSolrClient.getClusterStateProvider().getClusterProperty("urlScheme", "http"); - solrUrl = URLUtil.getBaseUrlForNodeName(firstLiveNode, urlScheme, false); - solrUrl = normalizeSolrUrl(solrUrl, false); - } + solrUrl = + solrUrlFromConnection( + solrConnection, cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION)); } } solrUrl = normalizeSolrUrl(solrUrl); return solrUrl; } + /** + * Resolves a base Solr URL from a parsed connection. The HTTP form (e.g. {@code -s + * http://host:port}) already names a Solr URL, so it is used directly without spinning up a + * CloudSolrClient; the ZooKeeper form queries the cluster for a live node's base URL. + */ + public static String solrUrlFromConnection( + CloudSolrClient.CloudSolrClientConnection solrConnection, String credentials) + throws Exception { + if (!solrConnection.isZookeeper()) { + return normalizeSolrUrl(solrConnection.quorumItems().get(0), false); + } + var builder = new HttpJettySolrClient.Builder().withOptionalBasicAuthCredentials(credentials); + try (CloudSolrClient cloudSolrClient = getCloudSolrClient(solrConnection, builder)) { + Set liveNodes = cloudSolrClient.getClusterState().getLiveNodes(); + if (liveNodes.isEmpty()) + throw new IllegalStateException( + "No live nodes found! Cannot determine 'solrUrl' from SolrCloud: " + solrConnection); + + String firstLiveNode = liveNodes.iterator().next(); + String urlScheme = + cloudSolrClient.getClusterStateProvider().getClusterProperty("urlScheme", "http"); + return normalizeSolrUrl( + URLUtil.getBaseUrlForNodeName(firstLiveNode, urlScheme, false), false); + } + } + /** * Returns true if the user supplied any of the connection-related CLI options ({@code * --solr-url}, {@code --solr-connection}, or {@code --zk-host}). Use this to gate logic that diff --git a/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java b/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java index 9362e070d817..4910b840465f 100644 --- a/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java +++ b/solr/core/src/java/org/apache/solr/cli/CliDefaultValueProvider.java @@ -25,6 +25,7 @@ public class CliDefaultValueProvider implements CommandLine.IDefaultValueProvide public String defaultValue(CommandLine.Model.ArgSpec argSpec) throws Exception { return switch (argSpec.paramLabel()) { case "" -> EnvUtils.getProperty("zkHost"); + case "" -> EnvUtils.getProperty("solr-connection"); case "" -> EnvUtils.getProperty("solr.url"); case "" -> EnvUtils.getProperty("solr.port", "8983"); case "" -> EnvUtils.getProperty("solr.max.wait.seconds", "0"); diff --git a/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java index 2fda9d9e8007..775e867c0771 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java +++ b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java @@ -16,17 +16,27 @@ */ package org.apache.solr.cli; +import org.apache.solr.client.solrj.impl.CloudSolrClient; import picocli.CommandLine; /** * Picocli ArgGroup for mutually-exclusive Solr URL / ZooKeeper connection options. * *

Use as the type of an {@code @ArgGroup(exclusive = true, multiplicity = "0..1")} field to - * ensure the user provides at most one of {@code --solr-url} or {@code --zk-host}. + * ensure the user provides at most one of {@code --solr-connection}, {@code --solr-url} or {@code + * --zk-host}. */ class ConnectionOptions { @CommandLine.Option( - names = {"-s", "--solr-url"}, + names = {"-s", "--solr-connection"}, + description = + "Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to " + + CommonCLIOptions.DefaultValues.ZK_HOST + + ".") + String solrConnection; + + @CommandLine.Option( + names = {"--solr-url"}, description = "Base Solr URL, which can be used to determine the zk-host if that's not known.") String solrUrl; @@ -38,4 +48,28 @@ class ConnectionOptions { + CommonCLIOptions.DefaultValues.ZK_HOST + ".") String zkHost; + + /** + * The effective ZooKeeper connection string, taking {@code --solr-connection} into account, or + * null if the user targeted Solr via a URL (or gave no target at all). + */ + String effectiveZkHost() throws java.io.IOException { + if (solrConnection != null) { + var connection = CloudSolrClient.CloudSolrClientConnection.parse(solrConnection); + return connection.isZookeeper() ? solrConnection : null; + } + return zkHost; + } + + /** + * The effective Solr URL, taking {@code --solr-connection} into account, or null if the user + * targeted ZooKeeper (or gave no target at all). + */ + String effectiveSolrUrl() throws java.io.IOException { + if (solrConnection != null) { + var connection = CloudSolrClient.CloudSolrClientConnection.parse(solrConnection); + return connection.isZookeeper() ? null : connection.quorumItems().get(0); + } + return solrUrl; + } } diff --git a/solr/core/src/java/org/apache/solr/cli/CreateTool.java b/solr/core/src/java/org/apache/solr/cli/CreateTool.java index 1538e634a7ae..d047eeed12e4 100644 --- a/solr/core/src/java/org/apache/solr/cli/CreateTool.java +++ b/solr/core/src/java/org/apache/solr/cli/CreateTool.java @@ -440,8 +440,10 @@ private void printDefaultConfigsetWarning( @Override public int callTool() throws Exception { String zkHostArg = - (connectionOptions != null) ? connectionOptions.zkHost : EnvUtils.getProperty("zkHost"); - String solrUrlArg = (connectionOptions != null) ? connectionOptions.solrUrl : null; + (connectionOptions != null) + ? connectionOptions.effectiveZkHost() + : EnvUtils.getProperty("zkHost"); + String solrUrlArg = (connectionOptions != null) ? connectionOptions.effectiveSolrUrl() : null; if (zkHostArg != null) { CreateParams params = diff --git a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java index a17b351b0a26..2d1a77c3fc10 100644 --- a/solr/core/src/java/org/apache/solr/cli/DeleteTool.java +++ b/solr/core/src/java/org/apache/solr/cli/DeleteTool.java @@ -279,8 +279,10 @@ private void deleteCore(DeleteParams params, SolrClient solrClient) throws Excep @Override public int callTool() throws Exception { String zkHostArg = - (connectionOptions != null) ? connectionOptions.zkHost : EnvUtils.getProperty("zkHost"); - String solrUrlArg = (connectionOptions != null) ? connectionOptions.solrUrl : null; + (connectionOptions != null) + ? connectionOptions.effectiveZkHost() + : EnvUtils.getProperty("zkHost"); + String solrUrlArg = (connectionOptions != null) ? connectionOptions.effectiveSolrUrl() : null; DeleteParams params = new DeleteParams(name, credentialsOptions.credentials, deleteConfig, force); diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index 0d6397a35e20..ff262d76ffab 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -57,7 +57,7 @@ @SuppressWarnings("UnnecessarilyFullyQualified") @picocli.CommandLine.Command( name = "solr", - version = "Apache Solr version " + SolrVersion.LATEST_STRING, + version = "Client version: " + SolrVersion.LATEST_STRING, mixinStandardHelpOptions = true, synopsisHeading = "usage: bin/", commandListHeading = "\nCommands:\n", diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index 01783a8de5a9..b878f6ea3895 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -31,6 +31,7 @@ import org.apache.commons.cli.Options; import org.apache.solr.cli.SolrProcessManager.SolrProcess; import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.request.ClusterApi; import org.apache.solr.client.solrj.request.CollectionsApi; import org.apache.solr.client.solrj.request.SystemInfoRequest; @@ -71,14 +72,40 @@ public class StatusTool extends ToolBase { description = "Wait up to the specified number of seconds to see Solr running.") private Integer maxWaitSecs; - @picocli.CommandLine.Option( - names = {"-p", "--port"}, - description = "Port on localhost to check status for") + @picocli.CommandLine.ArgGroup(exclusive = true, multiplicity = "0..1") + private TargetOptions targetOptions; + + static class TargetOptions { + @picocli.CommandLine.Option( + names = {"-p", "--port"}, + description = "Port on localhost to check status for") + private Integer port; + + @picocli.CommandLine.Option( + names = {"-s", "--solr-connection"}, + description = + "Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to " + + CommonCLIOptions.DefaultValues.ZK_HOST + + '.') + private String solrConnection; + + @picocli.CommandLine.Option( + names = {"--solr-url"}, + description = + "Base Solr URL, which can be used to determine the zk-host if that's not known") + private String solrUrl; + + @picocli.CommandLine.Option( + names = {"-z", "--zk-host"}, + description = + "Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to " + + CommonCLIOptions.DefaultValues.ZK_HOST + + '.') + private String zkHost; + } + private Integer port; - @picocli.CommandLine.Option( - names = {"-s", "--solr-url"}, - description = "Base Solr URL, which can be used to determine the zk-host if that's not known") private String solrUrl; @picocli.CommandLine.Option( @@ -400,6 +427,22 @@ private static Map getCloudStatus(SolrClient solrClient, String @Override public int callTool() throws Exception { + if (targetOptions != null) { + port = targetOptions.port; + if (targetOptions.solrUrl != null) { + solrUrl = targetOptions.solrUrl; + } else { + String connectionString = + targetOptions.solrConnection != null + ? targetOptions.solrConnection + : targetOptions.zkHost; + if (connectionString != null) { + solrUrl = + CLIUtils.solrUrlFromConnection( + CloudSolrClient.CloudSolrClientConnection.parse(connectionString), credentials); + } + } + } return runTool(); } } diff --git a/solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java b/solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java index 432d8cddd03e..c83a58ade0b6 100644 --- a/solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java +++ b/solr/core/src/java/org/apache/solr/cli/ZkConnectionOptions.java @@ -18,6 +18,7 @@ import java.util.Map; import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.impl.CloudSolrClient; import picocli.CommandLine; /** @@ -38,7 +39,15 @@ public class ZkConnectionOptions { public String zkHost; @CommandLine.Option( - names = {"-s", "--solr-url"}, + names = {"-s", "--solr-connection"}, + description = + "Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to " + + CommonCLIOptions.DefaultValues.ZK_HOST + + '.') + public String solrConnection; + + @CommandLine.Option( + names = {"--solr-url"}, description = "Base Solr URL, which can be used to determine the zk-host if --zk-host is not known") public String solrUrl; @@ -53,6 +62,8 @@ public class ZkConnectionOptions { * Resolves the ZooKeeper connection string using the following precedence: * *

    + *
  1. {@code --solr-connection} option value; used directly if it is a ZooKeeper connection + * string, otherwise treated as a Solr URL to query for the ZooKeeper host *
  2. Explicit {@code --zk-host} option value *
  3. ZooKeeper host derived by querying the Solr instance at {@code --solr-url} *
  4. ZooKeeper host derived by querying the default Solr URL ({@code http://localhost:8983}), @@ -64,6 +75,15 @@ public class ZkConnectionOptions { * @throws Exception if the Solr instance cannot be reached */ public String resolveZkHost() throws Exception { + if (solrConnection != null) { + var connection = CloudSolrClient.CloudSolrClientConnection.parse(solrConnection); + if (connection.isZookeeper()) { + return solrConnection; + } + // HTTP form: the connection string names a Solr URL, so ask that Solr for its ZooKeeper + return zkHostFromSolrUrl(CLIUtils.normalizeSolrUrl(connection.quorumItems().get(0))); + } + if (zkHost != null) { return zkHost; } @@ -72,11 +92,14 @@ public String resolveZkHost() throws Exception { if (resolvedSolrUrl == null) { resolvedSolrUrl = CLIUtils.getDefaultSolrUrl(); CLIO.err( - "Neither --zk-host or --solr-url parameters, nor ZK_HOST env var provided, so assuming solr url is " + "Neither --solr-connection, --zk-host or --solr-url parameters, nor SOLR_CONNECTION, ZK_HOST env var provided, so assuming solr url is " + resolvedSolrUrl + "."); } + return zkHostFromSolrUrl(resolvedSolrUrl); + } + private String zkHostFromSolrUrl(String resolvedSolrUrl) throws Exception { try (SolrClient solrClient = CLIUtils.getSolrClient(resolvedSolrUrl, credentials)) { Map status = StatusTool.reportStatus(solrClient); @SuppressWarnings("unchecked") diff --git a/solr/packaging/test/test_status.bats b/solr/packaging/test/test_status.bats index c03e07bfc60a..7ec9d865711a 100644 --- a/solr/packaging/test/test_status.bats +++ b/solr/packaging/test/test_status.bats @@ -53,7 +53,7 @@ teardown() { @test "multiple connection options are prevented" { run solr status --port ${SOLR_PORT} --solr-url http://localhost:${SOLR_PORT} - assert_output --partial "The option 'solr-url' was specified but an option from this group has already been selected: 'p'" + assert_output --regexp "(-s|solr-url).*(mutually exclusive|already been selected)" } @test "status with invalid --solr-url from user" { From 37846077c53fcabc185070f50a3834a6033efd43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 30 Jul 2026 16:50:18 +0200 Subject: [PATCH 16/19] Fix Error Prone UnnecessarilyFullyQualified violations failing CI Import IOException in ConnectionOptions and picocli.CommandLine in SolrCLITest instead of using fully qualified names. --- .../src/java/org/apache/solr/cli/ConnectionOptions.java | 5 +++-- solr/core/src/test/org/apache/solr/cli/SolrCLITest.java | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java index 775e867c0771..0a782b5a60fb 100644 --- a/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java +++ b/solr/core/src/java/org/apache/solr/cli/ConnectionOptions.java @@ -16,6 +16,7 @@ */ package org.apache.solr.cli; +import java.io.IOException; import org.apache.solr.client.solrj.impl.CloudSolrClient; import picocli.CommandLine; @@ -53,7 +54,7 @@ class ConnectionOptions { * The effective ZooKeeper connection string, taking {@code --solr-connection} into account, or * null if the user targeted Solr via a URL (or gave no target at all). */ - String effectiveZkHost() throws java.io.IOException { + String effectiveZkHost() throws IOException { if (solrConnection != null) { var connection = CloudSolrClient.CloudSolrClientConnection.parse(solrConnection); return connection.isZookeeper() ? solrConnection : null; @@ -65,7 +66,7 @@ String effectiveZkHost() throws java.io.IOException { * The effective Solr URL, taking {@code --solr-connection} into account, or null if the user * targeted ZooKeeper (or gave no target at all). */ - String effectiveSolrUrl() throws java.io.IOException { + String effectiveSolrUrl() throws IOException { if (solrConnection != null) { var connection = CloudSolrClient.CloudSolrClientConnection.parse(solrConnection); return connection.isZookeeper() ? null : connection.quorumItems().get(0); diff --git a/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java b/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java index 50cfcfed3cd0..57839924535b 100644 --- a/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java +++ b/solr/core/src/test/org/apache/solr/cli/SolrCLITest.java @@ -18,6 +18,7 @@ import org.apache.solr.SolrTestCase; import org.junit.Test; +import picocli.CommandLine; public class SolrCLITest extends SolrTestCase { @@ -39,7 +40,7 @@ public void testUptime() { @Test public void testFirstLineOnlyHelpFactoryTruncatesMultiLineDescription() { - picocli.CommandLine cmd = new picocli.CommandLine(new MultiLineDescriptionCommand()); + CommandLine cmd = new CommandLine(new MultiLineDescriptionCommand()); String baseline = cmd.getUsageMessage(); assertTrue("baseline should include first line", baseline.contains("First line.")); @@ -52,9 +53,9 @@ public void testFirstLineOnlyHelpFactoryTruncatesMultiLineDescription() { assertFalse("truncated should NOT include third line", truncated.contains("Third line.")); } - @picocli.CommandLine.Command(name = "test") + @CommandLine.Command(name = "test") private static class MultiLineDescriptionCommand { - @picocli.CommandLine.Option( + @CommandLine.Option( names = "--multi", description = {"First line.", "Second line.", "Third line."}) String multi; From 4ec40c9e27bccfde9d0cc67736b300a7d1ff797f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 30 Jul 2026 17:30:57 +0200 Subject: [PATCH 17/19] Fix dropped --credentials in StatusTool.waitToSeeSolrUp The picocli field refactor made the credential-taking overload read the (unset) field instead of its parameter, so AssertTool's status checks sent no Authorization header and test_basic_auth.bats failed with 401. --- solr/core/src/java/org/apache/solr/cli/StatusTool.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/core/src/java/org/apache/solr/cli/StatusTool.java b/solr/core/src/java/org/apache/solr/cli/StatusTool.java index b878f6ea3895..fb3c047b03a6 100644 --- a/solr/core/src/java/org/apache/solr/cli/StatusTool.java +++ b/solr/core/src/java/org/apache/solr/cli/StatusTool.java @@ -349,7 +349,7 @@ public Map waitToSeeSolrUp( while (System.nanoTime() < timeout) { try { - return getStatus(pidUrl); + return getStatus(pidUrl, credentials); } catch (Exception exc) { if (CLIUtils.exceptionIsAuthRelated(exc)) { throw exc; From 4ab0149c086e57399ed33723bee0bf82a5c48ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 30 Jul 2026 17:30:57 +0200 Subject: [PATCH 18/19] Fix CLI docs generation under Gradle 9 and regenerate CLI ref-guide pages Project.javaexec was removed in Gradle 9 (which arrived with the main merge); use injected ExecOperations as done in gradle/globals.gradle. Regenerated pages/cli to reflect the new -s/--solr-connection option group on status, create, delete and zk subcommands. --- solr/solr-ref-guide/build.gradle | 11 ++++++++++- .../deployment-guide/pages/cli/solr-create.adoc | 9 ++++++--- .../deployment-guide/pages/cli/solr-delete.adoc | 7 +++++-- .../deployment-guide/pages/cli/solr-status.adoc | 15 +++++++++++---- .../deployment-guide/pages/cli/solr-zk-cp.adoc | 11 +++++++---- .../pages/cli/solr-zk-downconfig.adoc | 9 ++++++--- .../deployment-guide/pages/cli/solr-zk-ls.adoc | 8 ++++++-- .../pages/cli/solr-zk-mkroot.adoc | 9 ++++++--- .../deployment-guide/pages/cli/solr-zk-mv.adoc | 8 ++++++-- .../deployment-guide/pages/cli/solr-zk-rm.adoc | 8 ++++++-- .../pages/cli/solr-zk-upconfig.adoc | 9 ++++++--- .../pages/cli/solr-zk-updateacls.adoc | 8 ++++++-- 12 files changed, 81 insertions(+), 31 deletions(-) diff --git a/solr/solr-ref-guide/build.gradle b/solr/solr-ref-guide/build.gradle index 278bb56a57f5..d3f519a54c8a 100644 --- a/solr/solr-ref-guide/build.gradle +++ b/solr/solr-ref-guide/build.gradle @@ -16,6 +16,8 @@ */ import groovy.json.JsonOutput +import javax.inject.Inject +import org.gradle.process.ExecOperations import org.apache.tools.ant.util.TeeOutputStream apply plugin: 'java' @@ -740,8 +742,15 @@ List buildCliNavEntries(File cliDir) { // Run ManPageGenerator and return the set of generated files (excluding the top-level solr.adoc). // SolrCLI is annotated @Command(name="solr"), so ManPageGenerator writes all files flat into outputDir. +interface CliDocsExecOperationsHolder { + @Inject + ExecOperations getExecOperations() +} + List runManPageGenerator(File outputDir) { - javaexec { + // Project.javaexec was removed in Gradle 9; use injected ExecOperations instead + def execOps = project.objects.newInstance(CliDocsExecOperationsHolder).execOperations + execOps.javaexec { classpath = configurations.cliClasspath mainClass = 'picocli.codegen.docgen.manpage.ManPageGenerator' args = ['--outdir', outputDir.absolutePath, 'org.apache.solr.cli.SolrCLI'] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc index 74417d2a1943..eb0537a83fb2 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-create.adoc @@ -35,7 +35,7 @@ bin/solr create - Creates a core or collection depending on whether Solr is runn .... bin/solr create [-hvV] -c= [-d=] [-n=] [-rf=] [-sh=] [-u=] - [-s= | -z=] + [-s= | --solr-url= | -z=] .... // end::picocli-generated-man-section-synopsis[] @@ -65,12 +65,15 @@ Creates a core or collection depending on whether Solr is running in standalone *-rf, --replication-factor*=__:: Number of copies of each document across the collection (replicas per shard); default is 1. -*-s*, *--solr-url*=__:: - Base Solr URL, which can be used to determine the zk-host if that's not known. +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. *-sh, --shards*=__:: Number of shards; default is 1. +*--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if that's not known. + *-u*, *--credentials*=__:: Credentials in the format username:password. Example: --credentials solr:SolrRocks diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc index a7601a9d606d..beec9f271b54 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-delete.adoc @@ -34,7 +34,7 @@ bin/solr delete - Deletes a collection or core depending on whether Solr is runn .... bin/solr delete [-fhvV] [--delete-config] -c= [-u=] - [-s= | -z=] + [-s= | --solr-url= | -z=] .... // end::picocli-generated-man-section-synopsis[] @@ -61,7 +61,10 @@ Deletes a collection or core depending on whether Solr is running in SolrCloud o *-h*, *--help*:: Show this help message and exit. -*-s*, *--solr-url*=__:: +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if that's not known. *-u*, *--credentials*=__:: diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-status.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-status.adoc index 59c45857277a..cf5dd062ee65 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-status.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-status.adoc @@ -33,8 +33,9 @@ bin/solr status - Get the status of a Solr server. == Synopsis .... -bin/solr status [-v] [--short] [--max-wait-secs=] [-p=] - [-s=] [-u=] +bin/solr status [-v] [--short] [--max-wait-secs=] [-u=] + [-p= | -s= | --solr-url= | + -z=] .... // end::picocli-generated-man-section-synopsis[] @@ -55,18 +56,24 @@ Get the status of a Solr server. *-p*, *--port*=__:: Port on localhost to check status for -*-s*, *--solr-url*=__:: - Base Solr URL, which can be used to determine the zk-host if that's not known +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. *--short*:: Short format. Prints one URL per line for running instances +*--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if that's not known + *-u*, *--credentials*=__:: Credentials in the format username:password. Example: --credentials solr:SolrRocks *-v*, *--verbose*:: Enable verbose mode. +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + // end::picocli-generated-man-section-options[] // tag::picocli-generated-man-section-arguments[] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc index 39fdfc642fbb..deb508993cf2 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-cp.adoc @@ -33,8 +33,8 @@ bin/solr zk cp - Copy files or folders to/from ZooKeeper or ZooKeeper to ZooKeep == Synopsis .... -bin/solr zk cp [-rv] [-s=] [--solr-home=DIR] [-u=] - [-z=] +bin/solr zk cp [-rv] [-s=] [--solr-home=DIR] [--solr-url=] + [-u=] [-z=] .... // end::picocli-generated-man-section-synopsis[] @@ -58,12 +58,15 @@ and the last element of the path will be appended unless also ends i *-r*, *--recursive*:: Apply the command recursively. -*-s*, *--solr-url*=__:: - Base Solr URL, which can be used to determine the zk-host if --zk-host is not known +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. *--solr-home*=_DIR_:: Required to look up configuration for compressing state.json. +*--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if --zk-host is not known + *-u*, *--credentials*=__:: Credentials in the format username:password. Example: --credentials solr:SolrRocks diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc index 342566fcf6a6..7ec72873a06e 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-downconfig.adoc @@ -33,8 +33,8 @@ bin/solr zk downconfig - Download a configset from ZooKeeper to the local filesy == Synopsis .... -bin/solr zk downconfig [-v] -d=DIR -n= [-s=] [-u=] - [-z=] +bin/solr zk downconfig [-v] -d=DIR -n= [-s=] + [--solr-url=] [-u=] [-z=] .... // end::picocli-generated-man-section-synopsis[] @@ -87,7 +87,10 @@ If a pre-existing configuration set is specified, it will be overwritten in ZooK + -*-s*, *--solr-url*=__:: +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known *-u*, *--credentials*=__:: diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc index fe3588d5abda..8cf2382dc8b3 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-ls.adoc @@ -33,7 +33,8 @@ bin/solr zk ls - List the contents of a ZooKeeper node. == Synopsis .... -bin/solr zk ls [-rv] [-s=] [-u=] [-z=] +bin/solr zk ls [-rv] [-s=] [--solr-url=] + [-u=] [-z=] .... // end::picocli-generated-man-section-synopsis[] @@ -51,7 +52,10 @@ List the contents of a ZooKeeper node. *-r*, *--recursive*:: Apply the command recursively. -*-s*, *--solr-url*=__:: +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known *-u*, *--credentials*=__:: diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc index 79582b93e27a..7d6027666ae3 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mkroot.adoc @@ -33,8 +33,8 @@ bin/solr zk mkroot - Make a znode in ZooKeeper with no data. Can be used to make == Synopsis .... -bin/solr zk mkroot [-v] [--fail-on-exists[=]] [-s=] - [-u=] [-z=] +bin/solr zk mkroot [-v] [--fail-on-exists[=]] [-s=] + [--solr-url=] [-u=] [-z=] .... // end::picocli-generated-man-section-synopsis[] @@ -52,7 +52,10 @@ Make a znode in ZooKeeper with no data. Can be used to make a path of arbitrary *--fail-on-exists*[=__]:: Raise an error if the znode already exists. Defaults to false. -*-s*, *--solr-url*=__:: +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known *-u*, *--credentials*=__:: diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc index 1a86d32719aa..5c8103a1383c 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-mv.adoc @@ -33,7 +33,8 @@ bin/solr zk mv - Move (rename) a znode on ZooKeeper. == Synopsis .... -bin/solr zk mv [-v] [-s=] [-u=] [-z=] +bin/solr zk mv [-v] [-s=] [--solr-url=] [-u=] + [-z=] .... // end::picocli-generated-man-section-synopsis[] @@ -52,7 +53,10 @@ and the last element of the path will be appended. // tag::picocli-generated-man-section-options[] == Options -*-s*, *--solr-url*=__:: +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known *-u*, *--credentials*=__:: diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc index b194510bbaec..64d7a58e32fb 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-rm.adoc @@ -33,7 +33,8 @@ bin/solr zk rm - Remove a znode from ZooKeeper. == Synopsis .... -bin/solr zk rm [-rv] [-s=] [-u=] [-z=] +bin/solr zk rm [-rv] [-s=] [--solr-url=] + [-u=] [-z=] .... // end::picocli-generated-man-section-synopsis[] @@ -51,7 +52,10 @@ Remove a znode from ZooKeeper. *-r*, *--recursive*:: Apply the command recursively. -*-s*, *--solr-url*=__:: +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known *-u*, *--credentials*=__:: diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc index 7353719867ff..20708dfe6788 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-upconfig.adoc @@ -33,8 +33,8 @@ bin/solr zk upconfig - Upload a configset from the local filesystem to ZooKeeper == Synopsis .... -bin/solr zk upconfig [-v] -d=DIR -n= [-s=] [-u=] - [-z=] +bin/solr zk upconfig [-v] -d=DIR -n= [-s=] + [--solr-url=] [-u=] [-z=] .... // end::picocli-generated-man-section-synopsis[] @@ -87,7 +87,10 @@ If a pre-existing configuration set is specified, it will be overwritten in ZooK + -*-s*, *--solr-url*=__:: +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known *-u*, *--credentials*=__:: diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc index da0c05f74370..43abe8ebeb74 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-zk-updateacls.adoc @@ -33,7 +33,8 @@ bin/solr zk updateacls - Recursively re-applies ZooKeeper ACLs to a znode and al == Synopsis .... -bin/solr zk updateacls [-v] [-s=] [-u=] [-z=] +bin/solr zk updateacls [-v] [-s=] [--solr-url=] + [-u=] [-z=] .... // end::picocli-generated-man-section-synopsis[] @@ -48,7 +49,10 @@ Recursively re-applies ZooKeeper ACLs to a znode and all its descendants. The AC // tag::picocli-generated-man-section-options[] == Options -*-s*, *--solr-url*=__:: +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-url*=__:: Base Solr URL, which can be used to determine the zk-host if --zk-host is not known *-u*, *--credentials*=__:: From 91c9460c019136eb35ba20c46d1c31048cf619cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Mon, 3 Aug 2026 11:23:18 +0200 Subject: [PATCH 19/19] SOLR-17697: Implement picocli for auth command / AuthTool (#4684) --- .../java/org/apache/solr/cli/AuthTool.java | 519 +++++++++++------- .../src/java/org/apache/solr/cli/SolrCLI.java | 1 + .../apache/solr/cli/AuthToolPicocliTest.java | 43 ++ .../org/apache/solr/cli/AuthToolTest.java | 10 +- .../deployment-guide/deployment-nav.adoc | 1 + .../deployment-guide/pages/cli/solr-auth.adoc | 126 +++++ 6 files changed, 501 insertions(+), 199 deletions(-) create mode 100644 solr/core/src/test/org/apache/solr/cli/AuthToolPicocliTest.java create mode 100644 solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-auth.adoc diff --git a/solr/core/src/java/org/apache/solr/cli/AuthTool.java b/solr/core/src/java/org/apache/solr/cli/AuthTool.java index 52a4335279f7..b63a0d545ddf 100644 --- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java +++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java @@ -32,11 +32,13 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.lucene.util.Constants; +import org.apache.solr.client.solrj.impl.SolrZkClientTimeout; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.util.EnvUtils; import org.apache.solr.core.SolrCore; @@ -44,6 +46,27 @@ import org.apache.zookeeper.KeeperException; /** Supports auth command in the bin/solr script. */ +@SuppressWarnings("UnnecessarilyFullyQualified") +@picocli.CommandLine.Command( + name = "auth", + description = + "Updates or enables/disables authentication. Must be run on the Solr server itself.", + exitCodeListHeading = "%nExit Codes:%n", + exitCodeList = { + "0:Authentication was enabled or disabled successfully.", + "1:Failed to enable or disable authentication; check output for details." + }, + footerHeading = "%nExamples:%n", + footer = { + " # Enable basic authentication with the given credentials", + " bin/solr auth enable --type basicAuth --credentials solr:SolrRocks", + "", + " # Enable basic authentication, prompting for credentials", + " bin/solr auth enable --type basicAuth --prompt true", + "", + " # Disable authentication", + " bin/solr auth disable" + }) public class AuthTool extends ToolBase { private static final Option TYPE_OPTION = @@ -103,6 +126,83 @@ public class AuthTool extends ToolBase { "This is where any authentication related configuration files, if any, would be placed. Defaults to $SOLR_HOME.") .get(); + /** + * Parameters for the auth sub-commands, independent of the command line parser. + * + * @param zkHost resolved ZooKeeper connection string, or null if {@code updateIncludeFileOnly} is + * set or the connection string could not be resolved + */ + record AuthParams( + boolean prompt, + String credentials, + String blockUnknown, + boolean updateIncludeFileOnly, + String solrIncludeFile, + String authConfDir, + boolean zkHostSpecified, + String zkHost) {} + + // --- picocli fields --- + + @picocli.CommandLine.Parameters( + index = "0", + arity = "1", + paramLabel = "enable|disable", + description = "Whether to enable or disable authentication.") + private String authCommand; + + @picocli.CommandLine.Option( + names = {"--type"}, + paramLabel = "TYPE", + defaultValue = "basicAuth", + description = + "The authentication mechanism to enable (currently only basicAuth). Defaults to 'basicAuth'.") + private String type; + + @picocli.CommandLine.Option( + names = {"--prompt"}, + paramLabel = "true|false", + description = + "Prompts the user to provide the credentials. Use either --credentials or --prompt, not both.") + private String promptOption; + + @picocli.CommandLine.Option( + names = {"--block-unknown"}, + paramLabel = "true|false", + description = + "Blocks all access for unknown users (requires authentication for all endpoints).") + private String blockUnknown; + + @picocli.CommandLine.Option( + names = {"--solr-include-file"}, + paramLabel = "FILE", + required = true, + description = + "The Solr include file which contains overridable environment variables for configuring Solr configurations. Defaults to solr.in.sh or solr.in.cmd.") + private String solrIncludeFile; + + @picocli.CommandLine.Option( + names = {"--update-include-file-only"}, + paramLabel = "true|false", + description = + "Only update the solr.in.sh or solr.in.cmd file, and skip actual enabling/disabling" + + " authentication (i.e. don't update security.json).") + private String updateIncludeFileOnly; + + @picocli.CommandLine.Option( + names = {"--auth-conf-dir"}, + paramLabel = "DIR", + required = true, + description = + "This is where any authentication related configuration files, if any, would be placed. Defaults to $SOLR_HOME.") + private String authConfDir; + + @picocli.CommandLine.Mixin ZkConnectionOptions zkOpts; + + public AuthTool() { + this(new DefaultToolRuntime()); + } + public AuthTool(ToolRuntime runtime) { super(runtime); } @@ -147,201 +247,178 @@ public Options getOptions() { .addOptionGroup(getConnectionOptions()); } - private void ensureArgumentIsValidBooleanIfPresent(CommandLine cli, Option option) { - if (cli.hasOption(option)) { - final String value = cli.getOptionValue(option); - if (!"true".equalsIgnoreCase(value) && !"false".equalsIgnoreCase(value)) { - echo( - "Argument [" - + option.getLongOpt() - + "] must be either true or false, but was [" - + value - + "]"); - runtime.exit(1); - } + private void ensureArgumentIsValidBooleanIfPresent(String optionName, String value) { + if (value != null && !"true".equalsIgnoreCase(value) && !"false".equalsIgnoreCase(value)) { + echo("Argument [" + optionName + "] must be either true or false, but was [" + value + "]"); + runtime.exit(1); } } - private void handleBasicAuth(CommandLine cli) throws Exception { - String cmd = cli.getArgs()[0]; - boolean prompt = Boolean.parseBoolean(cli.getOptionValue(PROMPT_OPTION, "false")); - boolean updateIncludeFileOnly = - Boolean.parseBoolean(cli.getOptionValue(UPDATE_INCLUDE_FILE_OPTION, "false")); + private void handleCommand(String cmd, AuthParams params) throws Exception { switch (cmd) { - case "enable": - { - if (!prompt && !cli.hasOption(CommonCLIOptions.CREDENTIALS_OPTION)) { - CLIO.out("Option --credentials or --prompt is required with enable."); - runtime.exit(1); - } else if (!prompt - && (cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION) == null - || !cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION).contains(":"))) { - CLIO.out("Option --credentials is not in correct format."); - runtime.exit(1); - } - - String zkHost = null; - - if (!updateIncludeFileOnly) { - try { - zkHost = CLIUtils.getZkHost(cli); - } catch (Exception ex) { - if (cli.hasOption(CommonCLIOptions.ZK_HOST_OPTION)) { - CLIO.out( - "Couldn't get ZooKeeper host. Please make sure that ZooKeeper is running and the correct zk-host has been passed in."); - } else { - CLIO.out( - "Couldn't get ZooKeeper host. Please make sure Solr is running in cloud mode, or a zk-host has been passed in."); - } - runtime.exit(1); - } - if (zkHost == null) { - if (cli.hasOption(CommonCLIOptions.ZK_HOST_OPTION)) { - CLIO.out( - "Couldn't get ZooKeeper host. Please make sure that ZooKeeper is running and the correct zk-host has been passed in."); - } else { - CLIO.out( - "Couldn't get ZooKeeper host. Please make sure Solr is running in cloud mode, or a zk-host has been passed in."); - } - runtime.exit(1); - } - - // check if security is already enabled or not - try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { - checkSecurityJsonExists(zkClient); - } - } - - String username, password; - if (cli.hasOption(CommonCLIOptions.CREDENTIALS_OPTION)) { - String credentials = cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION); - username = credentials.split(":")[0]; - password = credentials.split(":")[1]; - } else { - Console console = System.console(); - // keep prompting until they've entered a non-empty username & password - do { - username = console.readLine("Enter username: "); - } while (username == null || username.trim().isEmpty()); - username = username.trim(); - - do { - password = new String(console.readPassword("Enter password: ")); - } while (password.isEmpty()); - } - - if (username.equals(password) - && !EnvUtils.getPropertyAsBool( - Sha256AuthenticationProvider.ALLOW_USER_AS_PASSWORD_PROP, false)) { - CLIO.err( - "Error: username and password must not be identical." - + " This credential would never authenticate."); - runtime.exit(1); - } - - String resourceName = "security.json"; - final URL resource = SolrCore.class.getClassLoader().getResource(resourceName); - if (null == resource) { - throw new IllegalArgumentException("invalid resource name: " + resourceName); - } - - ObjectMapper mapper = new ObjectMapper(); - JsonNode securityJson1 = mapper.readTree(resource.openStream()); - // Only override blockUnknown if explicitly passed; otherwise let the template decide - if (cli.hasOption(BLOCK_UNKNOWN_OPTION)) { - boolean blockUnknown = Boolean.parseBoolean(cli.getOptionValue(BLOCK_UNKNOWN_OPTION)); - ((ObjectNode) securityJson1.get("authentication")).put("blockUnknown", blockUnknown); - } - JsonNode credentialsNode = securityJson1.get("authentication").get("credentials"); - ((ObjectNode) credentialsNode) - .put(username, Sha256AuthenticationProvider.getSaltedHashedValue(password)); - JsonNode userRoleNode = securityJson1.get("authorization").get("user-role"); - String[] predefinedRoles = {"superadmin", "admin", "search", "index"}; - ArrayNode rolesNode = mapper.createArrayNode(); - for (String role : predefinedRoles) { - rolesNode.add(role); - } - ((ObjectNode) userRoleNode).set(username, rolesNode); - String securityJson = securityJson1.toPrettyString(); - - if (!updateIncludeFileOnly) { - echoIfVerbose("Uploading following security.json: " + securityJson); - try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { - zkClient.makePath( - "/security.json", securityJson.getBytes(StandardCharsets.UTF_8), false); - } - } - - String solrIncludeFilename = cli.getOptionValue(SOLR_INCLUDE_FILE_OPTION); - Path includeFile = Path.of(solrIncludeFilename); - if (Files.notExists(includeFile) || !Files.isWritable(includeFile)) { - CLIO.out( - "Solr include file " + solrIncludeFilename + " doesn't exist or is not writeable."); - printAuthEnablingInstructions(username, password); - runtime.exit(0); - } - String authConfDir = cli.getOptionValue(AUTH_CONF_DIR_OPTION); - Path basicAuthConfFile = Path.of(authConfDir, "basicAuth.conf"); - - if (!Files.isWritable(basicAuthConfFile.getParent())) { - CLIO.out("Cannot write to file: " + basicAuthConfFile.toAbsolutePath()); - printAuthEnablingInstructions(username, password); - runtime.exit(0); - } - - Files.writeString( - basicAuthConfFile, - "httpBasicAuthUser=" + username + "\nhttpBasicAuthPassword=" + password, - StandardCharsets.UTF_8); - - // update the solr.in.sh file to contain the necessary authentication lines - updateIncludeFileEnableAuth(includeFile, basicAuthConfFile); - final String successMessage = - String.format( - Locale.ROOT, - "Successfully enabled basic auth with username [%s] assigned to all roles (superadmin, admin, index, search).", - username); - echo(successMessage); - if (!updateIncludeFileOnly) { - Map templateUsers = new LinkedHashMap<>(); - templateUsers.put("admin", "admin, index, search"); - templateUsers.put("index", "index, search"); - templateUsers.put("search", "search"); - templateUsers.remove(username); - CLIO.out( - "\nIMPORTANT: The following template users have been created with NO password set" - + " and cannot log in until passwords are assigned:"); - templateUsers.forEach((u, roles) -> CLIO.out(" - " + u + " (roles: " + roles + ")")); - CLIO.out( - "Set their passwords using the Admin UI Security page or the authentication API."); - } - return; - } - case "disable": - { - clearSecurityJson(cli, updateIncludeFileOnly); - - String solrIncludeFilename = cli.getOptionValue(SOLR_INCLUDE_FILE_OPTION); - Path includeFile = Path.of(solrIncludeFilename); - if (Files.notExists(includeFile) || !Files.isWritable(includeFile)) { - CLIO.out( - "Solr include file " + solrIncludeFilename + " doesn't exist or is not writeable."); - CLIO.out( - "Security has been disabled. Please remove any SOLR_AUTH_TYPE or SOLR_AUTHENTICATION_OPTS configuration from solr.in.sh/solr.in.cmd.\n"); - runtime.exit(0); - } - - // update the solr.in.sh file to comment out the necessary authentication lines - updateIncludeFileDisableAuth(includeFile); - return; - } - default: + case "enable" -> enableBasicAuth(params); + case "disable" -> disableBasicAuth(params); + default -> { CLIO.out("Valid auth commands are: enable, disable."); runtime.exit(1); + } + } + } + + private void enableBasicAuth(AuthParams params) throws Exception { + if (!params.prompt() && params.credentials() == null) { + CLIO.out("Option --credentials or --prompt is required with enable."); + runtime.exit(1); + } else if (!params.prompt() && !params.credentials().contains(":")) { + CLIO.out("Option --credentials is not in correct format."); + runtime.exit(1); } - CLIO.out("Options not understood."); - runtime.exit(1); + String zkHost = params.zkHost(); + + if (!params.updateIncludeFileOnly()) { + if (zkHost == null) { + printZkHostError(params.zkHostSpecified()); + runtime.exit(1); + } + + // check if security is already enabled or not + try (SolrZkClient zkClient = zkClient(zkHost)) { + checkSecurityJsonExists(zkClient); + } + } + + String username, password; + if (params.credentials() != null) { + username = params.credentials().split(":")[0]; + password = params.credentials().split(":")[1]; + } else { + Console console = System.console(); + // keep prompting until they've entered a non-empty username & password + do { + username = console.readLine("Enter username: "); + } while (username == null || username.trim().isEmpty()); + username = username.trim(); + + do { + password = new String(console.readPassword("Enter password: ")); + } while (password.isEmpty()); + } + + if (username.equals(password) + && !EnvUtils.getPropertyAsBool( + Sha256AuthenticationProvider.ALLOW_USER_AS_PASSWORD_PROP, false)) { + CLIO.err( + "Error: username and password must not be identical." + + " This credential would never authenticate."); + runtime.exit(1); + } + + String resourceName = "security.json"; + final URL resource = SolrCore.class.getClassLoader().getResource(resourceName); + if (null == resource) { + throw new IllegalArgumentException("invalid resource name: " + resourceName); + } + + ObjectMapper mapper = new ObjectMapper(); + JsonNode securityJson1 = mapper.readTree(resource.openStream()); + // Only override blockUnknown if explicitly passed; otherwise let the template decide + if (params.blockUnknown() != null) { + boolean blockUnknown = Boolean.parseBoolean(params.blockUnknown()); + ((ObjectNode) securityJson1.get("authentication")).put("blockUnknown", blockUnknown); + } + JsonNode credentialsNode = securityJson1.get("authentication").get("credentials"); + ((ObjectNode) credentialsNode) + .put(username, Sha256AuthenticationProvider.getSaltedHashedValue(password)); + JsonNode userRoleNode = securityJson1.get("authorization").get("user-role"); + String[] predefinedRoles = {"superadmin", "admin", "search", "index"}; + ArrayNode rolesNode = mapper.createArrayNode(); + for (String role : predefinedRoles) { + rolesNode.add(role); + } + ((ObjectNode) userRoleNode).set(username, rolesNode); + String securityJson = securityJson1.toPrettyString(); + + if (!params.updateIncludeFileOnly()) { + echoIfVerbose("Uploading following security.json: " + securityJson); + try (SolrZkClient zkClient = zkClient(zkHost)) { + zkClient.makePath("/security.json", securityJson.getBytes(StandardCharsets.UTF_8), false); + } + } + + Path includeFile = Path.of(params.solrIncludeFile()); + if (Files.notExists(includeFile) || !Files.isWritable(includeFile)) { + CLIO.out( + "Solr include file " + params.solrIncludeFile() + " doesn't exist or is not writeable."); + printAuthEnablingInstructions(username, password); + runtime.exit(0); + } + Path basicAuthConfFile = Path.of(params.authConfDir(), "basicAuth.conf"); + + if (!Files.isWritable(basicAuthConfFile.getParent())) { + CLIO.out("Cannot write to file: " + basicAuthConfFile.toAbsolutePath()); + printAuthEnablingInstructions(username, password); + runtime.exit(0); + } + + Files.writeString( + basicAuthConfFile, + "httpBasicAuthUser=" + username + "\nhttpBasicAuthPassword=" + password, + StandardCharsets.UTF_8); + + // update the solr.in.sh file to contain the necessary authentication lines + updateIncludeFileEnableAuth(includeFile, basicAuthConfFile); + final String successMessage = + String.format( + Locale.ROOT, + "Successfully enabled basic auth with username [%s] assigned to all roles (superadmin, admin, index, search).", + username); + echo(successMessage); + if (!params.updateIncludeFileOnly()) { + Map templateUsers = new LinkedHashMap<>(); + templateUsers.put("admin", "admin, index, search"); + templateUsers.put("index", "index, search"); + templateUsers.put("search", "search"); + templateUsers.remove(username); + CLIO.out( + "\nIMPORTANT: The following template users have been created with NO password set" + + " and cannot log in until passwords are assigned:"); + templateUsers.forEach((u, roles) -> CLIO.out(" - " + u + " (roles: " + roles + ")")); + CLIO.out("Set their passwords using the Admin UI Security page or the authentication API."); + } + } + + private void disableBasicAuth(AuthParams params) throws Exception { + clearSecurityJson(params); + + Path includeFile = Path.of(params.solrIncludeFile()); + if (Files.notExists(includeFile) || !Files.isWritable(includeFile)) { + CLIO.out( + "Solr include file " + params.solrIncludeFile() + " doesn't exist or is not writeable."); + CLIO.out( + "Security has been disabled. Please remove any SOLR_AUTH_TYPE or SOLR_AUTHENTICATION_OPTS configuration from solr.in.sh/solr.in.cmd.\n"); + runtime.exit(0); + } + + // update the solr.in.sh file to comment out the necessary authentication lines + updateIncludeFileDisableAuth(includeFile); + } + + private void printZkHostError(boolean zkHostSpecified) { + if (zkHostSpecified) { + CLIO.out( + "Couldn't get ZooKeeper host. Please make sure that ZooKeeper is running and the correct zk-host has been passed in."); + } else { + CLIO.out( + "Couldn't get ZooKeeper host. Please make sure Solr is running in cloud mode, or a zk-host has been passed in."); + } + } + + private static SolrZkClient zkClient(String zkHost) { + return new SolrZkClient.Builder() + .withUrl(zkHost) + .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT, TimeUnit.MILLISECONDS) + .build(); } private void checkSecurityJsonExists(SolrZkClient zkClient) @@ -357,10 +434,9 @@ private void checkSecurityJsonExists(SolrZkClient zkClient) } } - private void clearSecurityJson(CommandLine cli, boolean updateIncludeFileOnly) throws Exception { - String zkHost; - if (!updateIncludeFileOnly) { - zkHost = CLIUtils.getZkHost(cli); + private void clearSecurityJson(AuthParams params) throws Exception { + if (!params.updateIncludeFileOnly()) { + String zkHost = params.zkHost(); if (zkHost == null) { runtime.print("ZK Host not found. Solr should be running in cloud mode."); runtime.exit(1); @@ -368,7 +444,7 @@ private void clearSecurityJson(CommandLine cli, boolean updateIncludeFileOnly) t echoIfVerbose("Uploading following security.json: {}"); - try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) { + try (SolrZkClient zkClient = zkClient(zkHost)) { zkClient.makePath("/security.json", "{}".getBytes(StandardCharsets.UTF_8), false); } } @@ -470,13 +546,35 @@ private void updateIncludeFileDisableAuth(Path includeFile) throws IOException { @Override public void runImpl(CommandLine cli) throws Exception { - ensureArgumentIsValidBooleanIfPresent(cli, BLOCK_UNKNOWN_OPTION); - ensureArgumentIsValidBooleanIfPresent(cli, UPDATE_INCLUDE_FILE_OPTION); + ensureArgumentIsValidBooleanIfPresent( + BLOCK_UNKNOWN_OPTION.getLongOpt(), cli.getOptionValue(BLOCK_UNKNOWN_OPTION)); + ensureArgumentIsValidBooleanIfPresent( + UPDATE_INCLUDE_FILE_OPTION.getLongOpt(), cli.getOptionValue(UPDATE_INCLUDE_FILE_OPTION)); String type = cli.getOptionValue(TYPE_OPTION, "basicAuth"); // switch structure is here to support future auth options like oAuth if (type.equals("basicAuth")) { - handleBasicAuth(cli); + boolean updateIncludeFileOnly = + Boolean.parseBoolean(cli.getOptionValue(UPDATE_INCLUDE_FILE_OPTION, "false")); + String zkHost = null; + if (!updateIncludeFileOnly) { + try { + zkHost = CLIUtils.getZkHost(cli); + } catch (Exception e) { + echoIfVerbose("Could not resolve ZooKeeper host: " + e.getMessage()); + } + } + AuthParams params = + new AuthParams( + Boolean.parseBoolean(cli.getOptionValue(PROMPT_OPTION, "false")), + cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION), + cli.getOptionValue(BLOCK_UNKNOWN_OPTION), + updateIncludeFileOnly, + cli.getOptionValue(SOLR_INCLUDE_FILE_OPTION), + cli.getOptionValue(AUTH_CONF_DIR_OPTION), + cli.hasOption(CommonCLIOptions.ZK_HOST_OPTION), + zkHost); + handleCommand(cli.getArgs()[0], params); } else { throw new IllegalStateException("Only type=basicAuth supported at the moment."); } @@ -484,6 +582,35 @@ public void runImpl(CommandLine cli) throws Exception { @Override public int callTool() throws Exception { - throw new UnsupportedOperationException("This tool does not yet support PicoCli"); + ensureArgumentIsValidBooleanIfPresent(BLOCK_UNKNOWN_OPTION.getLongOpt(), blockUnknown); + ensureArgumentIsValidBooleanIfPresent( + UPDATE_INCLUDE_FILE_OPTION.getLongOpt(), updateIncludeFileOnly); + + // switch structure is here to support future auth options like oAuth + if (!"basicAuth".equals(type)) { + throw new IllegalStateException("Only type=basicAuth supported at the moment."); + } + + boolean updateIncludeFileOnlyBool = Boolean.parseBoolean(updateIncludeFileOnly); + String zkHost = null; + if (!updateIncludeFileOnlyBool) { + try { + zkHost = zkOpts.resolveZkHost(); + } catch (Exception e) { + echoIfVerbose("Could not resolve ZooKeeper host: " + e.getMessage()); + } + } + AuthParams params = + new AuthParams( + Boolean.parseBoolean(promptOption), + zkOpts.credentials, + blockUnknown, + updateIncludeFileOnlyBool, + solrIncludeFile, + authConfDir, + zkOpts.zkHost != null || zkOpts.solrConnection != null, + zkHost); + handleCommand(authCommand, params); + return 0; } } diff --git a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java index ff262d76ffab..266bb08badb6 100755 --- a/solr/core/src/java/org/apache/solr/cli/SolrCLI.java +++ b/solr/core/src/java/org/apache/solr/cli/SolrCLI.java @@ -80,6 +80,7 @@ StatusTool.class, VersionTool.class, ZkTool.class, + AuthTool.class, CreateTool.class, DeleteTool.class }) diff --git a/solr/core/src/test/org/apache/solr/cli/AuthToolPicocliTest.java b/solr/core/src/test/org/apache/solr/cli/AuthToolPicocliTest.java new file mode 100644 index 000000000000..41a4385b55da --- /dev/null +++ b/solr/core/src/test/org/apache/solr/cli/AuthToolPicocliTest.java @@ -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.cli; + +import java.util.Arrays; +import picocli.CommandLine; + +/** + * Runs all {@link AuthToolTest} tests through the picocli invocation path. + * + *

    All {@code @Test} methods are inherited from {@link AuthToolTest}. Only the tool invocation + * strategy is overridden here to use {@code picocli.CommandLine.execute()} instead of the + * commons-cli path. + */ +public class AuthToolPicocliTest extends AuthToolTest { + + @Override + protected int runTool(String[] args, Class clazz) throws Exception { + // args[0] is the tool name used by commons-cli dispatch; strip it for picocli. + String[] toolArgs = Arrays.copyOfRange(args, 1, args.length); + // Use a TestingRuntime so runtime.exit() cannot terminate the test JVM + ToolRuntime runtime = new CLITestHelper.TestingRuntime(false); + ToolBase tool = clazz.getDeclaredConstructor(ToolRuntime.class).newInstance(runtime); + return new CommandLine(tool) + .setDefaultValueProvider(new CliDefaultValueProvider()) + .execute(toolArgs); + } +} diff --git a/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java b/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java index aa31392a2f4f..b0e94f2ef8da 100644 --- a/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java +++ b/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java @@ -60,6 +60,10 @@ public void tearDown() throws Exception { } } + protected int runTool(String[] args, Class clazz) throws Exception { + return CLITestHelper.runTool(args, clazz); + } + @Test public void testEnableAuth() throws Exception { Path solrIncludeFile = Files.createFile(dir.resolve("solrIncludeFile.txt")); @@ -77,7 +81,7 @@ public void testEnableAuth() throws Exception { "--block-unknown", "true" }; - assertEquals(0, CLITestHelper.runTool(args, AuthTool.class)); + assertEquals(0, runTool(args, AuthTool.class)); } @Test @@ -95,7 +99,7 @@ public void testEnableAuthRejectsUsernameEqualPassword() throws Exception { "--credentials", "solr:solr" }; - assertNotEquals(0, CLITestHelper.runTool(args, AuthTool.class)); + assertNotEquals(0, runTool(args, AuthTool.class)); } @Test @@ -115,7 +119,7 @@ public void testEnableAuthAllowsUsernameEqualPasswordWithEscapeHatch() throws Ex "--credentials", "solr:solr" }; - assertEquals(0, CLITestHelper.runTool(args, AuthTool.class)); + assertEquals(0, runTool(args, AuthTool.class)); } finally { System.clearProperty(Sha256AuthenticationProvider.ALLOW_USER_AS_PASSWORD_PROP); } diff --git a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc index 65577fa5d725..5f83b0407404 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/deployment-nav.adoc @@ -19,6 +19,7 @@ // CLI-DOCS-START * xref:solr-control-script-reference.adoc[] +** xref:cli/solr-auth.adoc[auth] ** xref:cli/solr-create.adoc[create] ** xref:cli/solr-delete.adoc[delete] ** xref:cli/solr-start.adoc[start] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-auth.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-auth.adoc new file mode 100644 index 000000000000..1753b3042cb3 --- /dev/null +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/cli/solr-auth.adoc @@ -0,0 +1,126 @@ +// 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. +// +// DO NOT EDIT -- this page is auto-generated from picocli annotations. +// To update: modify the @Command/@Option annotations in the Java source, then run: +// ./gradlew :solr:solr-ref-guide:generateCliDocs + += bin/solr auth +:page-toclevels: 2 + +// tag::picocli-generated-man-section-name[] +== Name + +bin/solr auth - Updates or enables/disables authentication. Must be run on the Solr server itself. + +// end::picocli-generated-man-section-name[] + +// tag::picocli-generated-man-section-synopsis[] +== Synopsis + +.... +bin/solr auth [-v] --auth-conf-dir=DIR [--block-unknown=true|false] + [--prompt=true|false] [-s=] --solr-include-file=FILE + [--solr-url=] [--type=TYPE] [-u=] + [--update-include-file-only=true|false] [-z=] enable|disable +.... + +// end::picocli-generated-man-section-synopsis[] + +// tag::picocli-generated-man-section-description[] +== Description + +Updates or enables/disables authentication. Must be run on the Solr server itself. + +// end::picocli-generated-man-section-description[] + +// tag::picocli-generated-man-section-options[] +== Options + +*--auth-conf-dir*=_DIR_:: + *(required)* This is where any authentication related configuration files, if any, would be placed. Defaults to $SOLR_HOME. + +*--block-unknown*=_true|false_:: + Blocks all access for unknown users (requires authentication for all endpoints). + +*--prompt*=_true|false_:: + Prompts the user to provide the credentials. Use either --credentials or --prompt, not both. + +*-s*, *--solr-connection*=__:: + Zookeeper or HTTP(s) connection string; unnecessary if SOLR_CONNECTION is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +*--solr-include-file*=_FILE_:: + *(required)* The Solr include file which contains overridable environment variables for configuring Solr configurations. Defaults to solr.in.sh or solr.in.cmd. + +*--solr-url*=__:: + Base Solr URL, which can be used to determine the zk-host if --zk-host is not known + +*--type*=_TYPE_:: + The authentication mechanism to enable (currently only basicAuth). Defaults to 'basicAuth'. + +*-u*, *--credentials*=__:: + Credentials in the format username:password. Example: --credentials solr:SolrRocks + +*--update-include-file-only*=_true|false_:: + Only update the solr.in.sh or solr.in.cmd file, and skip actual enabling/disabling authentication (i.e. don't update security.json). + +*-v*, *--verbose*:: + Enable verbose mode. + +*-z*, *--zk-host*=__:: + Zookeeper connection string; unnecessary if ZK_HOST is defined in solr.in.sh; otherwise, defaults to localhost:9983. + +// end::picocli-generated-man-section-options[] + +// tag::picocli-generated-man-section-arguments[] +== Arguments + +_enable|disable_:: + Whether to enable or disable authentication. + +// end::picocli-generated-man-section-arguments[] + +// tag::picocli-generated-man-section-commands[] +// end::picocli-generated-man-section-commands[] + +// tag::picocli-generated-man-section-exit-status[] +== Exit Codes: + +*0*:: + Authentication was enabled or disabled successfully. + +*1*:: + Failed to enable or disable authentication; check output for details. + +// end::picocli-generated-man-section-exit-status[] + +// tag::picocli-generated-man-section-footer[] +== Examples: + +[%hardbreaks] + # Enable basic authentication with the given credentials + bin/solr auth enable --type basicAuth --credentials solr:SolrRocks + +[%hardbreaks] + # Enable basic authentication, prompting for credentials + bin/solr auth enable --type basicAuth --prompt true + +[%hardbreaks] + # Disable authentication + bin/solr auth disable + +// end::picocli-generated-man-section-footer[]