This is an automated email from the ASF dual-hosted git repository.
houston pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-operator.git
The following commit(s) were added to refs/heads/main by this push:
new 494471b Add default permission for replica balancing (#770)
494471b is described below
commit 494471b1b9ef1f921f16f348ee0b48bf2286f96d
Author: Houston Putman <[email protected]>
AuthorDate: Wed Mar 19 12:10:46 2025 -0500
Add default permission for replica balancing (#770)
"collection-admin-edit" for Solr 9.9+ and
"/____v2/cluster/replicas/balance" for previous versions
---
controllers/util/solr_security_util.go | 7 +++-
docs/solr-cloud/solr-cloud-crd.md | 16 +++++++++
tests/e2e/solrcloud_security_json_test.go | 59 +++++++++++++++++++++++++++----
tests/e2e/suite_test.go | 35 +++++++++++++++++-
tests/e2e/test_utils_test.go | 40 +++++++++++++--------
tests/scripts/manage_e2e_tests.sh | 2 +-
6 files changed, 134 insertions(+), 25 deletions(-)
diff --git a/controllers/util/solr_security_util.go
b/controllers/util/solr_security_util.go
index eab99d2..417d6b9 100644
--- a/controllers/util/solr_security_util.go
+++ b/controllers/util/solr_security_util.go
@@ -117,6 +117,9 @@ func reconcileForBasicAuthWithBootstrappedSecurityJson(ctx
context.Context, clie
// supply the bootstrap security.json to the initContainer via
a simple BASE64 encoding env var
security.SecurityJson =
string(bootstrapSecret.Data[SecurityJsonFile])
+ security.SecurityJsonSrc = &corev1.EnvVarSource{
+ SecretKeyRef: &corev1.SecretKeySelector{
+ LocalObjectReference:
corev1.LocalObjectReference{Name: bootstrapSecret.Name}, Key: SecurityJsonFile}}
basicAuthSecret = authSecret
}
@@ -393,7 +396,9 @@ func generateSecurityJson(solrCloud *solr.SolrCloud)
map[string][]byte {
{ "name": "k8s-metrics", "role":"k8s", "collection": null,
"path":"/admin/metrics" },
{ "name": "k8s-zk", "role":"k8s", "collection": null,
"path":"/admin/zookeeper/status" },
{ "name": "k8s-ping", "role":"k8s", "collection": "*",
"path":"/admin/ping" },
- { "name": "read", "role":["admin","users"] },
+ { "name": "k8s-replica-balancing", "role":"k8s", "collection": null,
"path":"/____v2/cluster/replicas/balance" },
+ { "name": "collection-admin-edit", "role":"k8s" },
+ { "name": "read", "role":["admin","users","k8s"] },
{ "name": "update", "role":["admin"] },
{ "name": "security-read", "role": ["admin"] },
{ "name": "security-edit", "role": ["admin"] },
diff --git a/docs/solr-cloud/solr-cloud-crd.md
b/docs/solr-cloud/solr-cloud-crd.md
index 5eaf6e9..52027f0 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -1032,6 +1032,16 @@ Take a moment to review these authorization rules so
that you're aware of the ro
"collection": "*",
"path": "/admin/ping"
},
+ {
+ "name": "k8s-replica-balancing",
+ "role": "k8s",
+ "collection": null,
+ "path": "/____v2/cluster/replicas/balance"
+ },
+ {
+ "name": "collection-admin-edit",
+ "role": "k8s"
+ },
{
"name": "read",
"role": [ "admin", "users" ]
@@ -1165,6 +1175,12 @@ Users need to ensure their `security.json` contains the
user supplied in the `ba
/admin/metrics
/admin/ping (for collection="*")
/admin/zookeeper/status
+/____v2/cluster/replicas/balance
+```
+
+And the following named permissions:
+```aiignore
+collection-admin-edit
```
_Tip: see the authorization rules defined by the default `security.json` as a
guide for configuring access for the operator user_
diff --git a/tests/e2e/solrcloud_security_json_test.go
b/tests/e2e/solrcloud_security_json_test.go
index 1ceef5e..8bc8695 100644
--- a/tests/e2e/solrcloud_security_json_test.go
+++ b/tests/e2e/solrcloud_security_json_test.go
@@ -20,9 +20,14 @@ package e2e
import (
"context"
solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
+ "github.com/apache/solr-operator/controllers"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
+ appsv1 "k8s.io/api/apps/v1"
+ corev1 "k8s.io/api/core/v1"
"k8s.io/utils/pointer"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "time"
)
var _ = FDescribe("E2E - SolrCloud - Security JSON", func() {
@@ -35,10 +40,6 @@ var _ = FDescribe("E2E - SolrCloud - Security JSON", func() {
})
JustBeforeEach(func(ctx context.Context) {
- By("generating the security.json secret and basic auth secret")
- generateSolrSecuritySecret(ctx, solrCloud)
- generateSolrBasicAuthSecret(ctx, solrCloud)
-
By("creating the SolrCloud")
Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())
@@ -50,20 +51,64 @@ var _ = FDescribe("E2E - SolrCloud - Security JSON", func()
{
solrCloud = expectSolrCloudToBeReady(ctx, solrCloud)
By("creating a first Solr Collection")
- createAndQueryCollection(ctx, solrCloud, "basic", 1, 1)
+ createAndQueryCollection(ctx, solrCloud, "basic", 2, 1)
})
- FContext("Provided Zookeeper", func() {
- BeforeEach(func() {
+ FContext("Provided Security JSON", func() {
+ BeforeEach(func(ctx context.Context) {
solrCloud.Spec.ZookeeperRef = &solrv1beta1.ZookeeperRef{
ProvidedZookeeper: &solrv1beta1.ZookeeperSpec{
Replicas: pointer.Int32(1),
Ephemeral: &solrv1beta1.ZKEphemeral{},
},
}
+
+ solrCloud.Spec.SolrSecurity =
&solrv1beta1.SolrSecurityOptions{
+ AuthenticationType: "Basic",
+ BasicAuthSecret: solrCloud.Name +
"-basic-auth-secret",
+ BootstrapSecurityJson:
&corev1.SecretKeySelector{
+ LocalObjectReference:
corev1.LocalObjectReference{
+ Name: solrCloud.Name +
"-security-secret",
+ },
+ Key: "security.json",
+ },
+ }
+
+ By("generating the security.json secret and basic auth
secret")
+ generateSolrSecuritySecret(ctx, solrCloud)
+ generateSolrBasicAuthSecret(ctx, solrCloud)
})
// All testing will be done in the "JustBeforeEach" logic, no
additional tests required here
FIt("Starts correctly", func(ctx context.Context) {})
})
+
+ FContext("Bootstrapped Security", func() {
+
+ BeforeEach(func() {
+ solrCloud.Spec.SolrSecurity =
&solrv1beta1.SolrSecurityOptions{
+ AuthenticationType: "Basic",
+ }
+ })
+
+ FIt("Scales up with replica migration", func(ctx
context.Context) {
+ originalSolrCloud := solrCloud.DeepCopy()
+ solrCloud.Spec.Replicas = pointer.Int32(int32(2))
+ By("triggering a scale up via solrCloud replicas")
+ Expect(k8sClient.Patch(ctx, solrCloud,
client.MergeFrom(originalSolrCloud))).To(Succeed(), "Could not patch SolrCloud
replicas to initiate scale down")
+
+ By("make sure scaleDown happens without a clusterLock
and eventually the replicas are removed")
+ // Once the scale down actually occurs, the statefulSet
annotations should be removed very soon
+ expectStatefulSetWithChecksAndTimeout(ctx, solrCloud,
solrCloud.StatefulSetName(), time.Minute*2, time.Millisecond*500, func(g
Gomega, found *appsv1.StatefulSet) {
+
g.Expect(found.Spec.Replicas).To(HaveValue(BeEquivalentTo(2)), "StatefulSet
should eventually have 2 pods.")
+ clusterOp, err :=
controllers.GetCurrentClusterOp(found)
+ g.Expect(err).ToNot(HaveOccurred(), "Error
occurred while finding clusterLock for SolrCloud")
+ g.Expect(clusterOp).To(BeNil(), "StatefulSet
should have a ScaleDown lock after scaling is complete.")
+ })
+
+ queryCollection(ctx, solrCloud, "basic", 0)
+
+ // TODO: When balancing is in all Operator supported
Solr versions, add a test to make sure balancing occurred
+ })
+ })
})
diff --git a/tests/e2e/suite_test.go b/tests/e2e/suite_test.go
index 1c2aec5..c746443 100644
--- a/tests/e2e/suite_test.go
+++ b/tests/e2e/suite_test.go
@@ -106,6 +106,7 @@ var _ = SynchronizedBeforeSuite(func(ctx context.Context) {
var err error
k8sConfig, err = config.GetConfig()
Expect(err).NotTo(HaveOccurred(), "Could not load in default kubernetes
config")
+ k8sConfig.Timeout = time.Minute
Expect(zkApi.AddToScheme(scheme.Scheme)).To(Succeed())
Expect(certManagerApi.AddToScheme(scheme.Scheme)).To(Succeed())
k8sClient, err = client.New(k8sConfig, client.Options{Scheme:
scheme.Scheme})
@@ -338,7 +339,7 @@ func writeAllSolrInfoToFiles(ctx context.Context, directory
string, namespace st
}
foundServices := &corev1.ServiceList{}
- Expect(k8sClient.List(ctx, foundServices, listOps)).To(Succeed(),
"Could not fetch Solr pods")
+ Expect(k8sClient.List(ctx, foundServices, listOps)).To(Succeed(),
"Could not fetch Solr services")
Expect(foundServices).ToNot(BeNil(), "No Solr services could be found")
for _, service := range foundServices.Items {
writeAllServiceInfoToFiles(
@@ -346,6 +347,25 @@ func writeAllSolrInfoToFiles(ctx context.Context,
directory string, namespace st
&service,
)
}
+
+ // Unfortunately the secrets don't have a technology label
+ req, err = labels.NewRequirement("solr-cloud", selection.Exists,
make([]string, 0))
+ Expect(err).ToNot(HaveOccurred())
+
+ labelSelector = labels.Everything().Add(*req)
+ listOps = &client.ListOptions{
+ Namespace: namespace,
+ LabelSelector: labelSelector,
+ }
+
+ foundSecrets := &corev1.SecretList{}
+ Expect(k8sClient.List(ctx, foundSecrets, listOps)).To(Succeed(), "Could
not fetch Solr secrets")
+ for _, secret := range foundSecrets.Items {
+ writeAllSecretInfoToFiles(
+ directory+secret.Name+".secret",
+ &secret,
+ )
+ }
}
// writeSolrClusterStatusInfoToFile writes the following each to a separate
file with the given base name & directory.
@@ -401,6 +421,19 @@ func writeAllServiceInfoToFiles(baseFilename string,
service *corev1.Service) {
Expect(writeErr).ToNot(HaveOccurred(), "Could not write service json to
file")
}
+// writeAllSecretInfoToFiles writes the following each to a separate file with
the given base name & directory.
+// - Service
+func writeAllSecretInfoToFiles(baseFilename string, secret *corev1.Secret) {
+ // Write service to a file
+ statusFile, err := os.Create(baseFilename + ".json")
+ defer statusFile.Close()
+ Expect(err).ToNot(HaveOccurred(), "Could not open file to save secret
status: %s", baseFilename+".json")
+ jsonBytes, marshErr := json.MarshalIndent(secret, "", "\t")
+ Expect(marshErr).ToNot(HaveOccurred(), "Could not serialize secret
json")
+ _, writeErr := statusFile.Write(jsonBytes)
+ Expect(writeErr).ToNot(HaveOccurred(), "Could not write secret json to
file")
+}
+
// writeAllPodInfoToFile writes the following each to a separate file with the
given base name & directory.
// - Pod Spec/Status
// - Pod Events
diff --git a/tests/e2e/test_utils_test.go b/tests/e2e/test_utils_test.go
index 203b93e..240ee4c 100644
--- a/tests/e2e/test_utils_test.go
+++ b/tests/e2e/test_utils_test.go
@@ -25,6 +25,7 @@ import (
"fmt"
"io"
"os"
+ "regexp"
"strconv"
"strings"
"time"
@@ -233,7 +234,7 @@ func createAndQueryCollectionWithGomega(ctx
context.Context, solrCloud *solrv1be
}
additionalOffset += 1
- g.EventuallyWithOffset(additionalOffset, func(innerG Gomega) {
+ g.EventuallyWithOffset(additionalOffset, func(innerG Gomega, ctx
context.Context) {
response, err := callSolrApiInPod(
ctx,
solrCloud,
@@ -246,7 +247,7 @@ func createAndQueryCollectionWithGomega(ctx
context.Context, solrCloud *solrv1be
}).Within(time.Second*10).WithContext(ctx).Should(Succeed(),
"Collection creation command start was not successful")
// Only wait 5 seconds when trying to create the asyncCommand
- g.EventuallyWithOffset(additionalOffset, func(innerG Gomega) {
+ g.EventuallyWithOffset(additionalOffset, func(innerG Gomega, ctx
context.Context) {
response, err := callSolrApiInPod(
ctx,
solrCloud,
@@ -271,7 +272,7 @@ func createAndQueryCollectionWithGomega(ctx
context.Context, solrCloud *solrv1be
innerG.Expect(response).To(ContainSubstring("\"state\":\"completed\""), "Did
not finish creating Solr Collection in time")
}).Within(time.Second*40).WithContext(ctx).Should(Succeed(),
"Collection creation was not successful")
- g.EventuallyWithOffset(additionalOffset, func(innerG Gomega) {
+ g.EventuallyWithOffset(additionalOffset, func(innerG Gomega, ctx
context.Context) {
response, err := callSolrApiInPod(
ctx,
solrCloud,
@@ -296,7 +297,7 @@ func queryCollection(ctx context.Context, solrCloud
*solrv1beta1.SolrCloud, coll
}
func queryCollectionWithGomega(ctx context.Context, solrCloud
*solrv1beta1.SolrCloud, collection string, docCount int, g Gomega,
additionalOffset ...int) {
- g.EventuallyWithOffset(resolveOffset(additionalOffset), func(innerG
Gomega) {
+ g.EventuallyWithOffset(resolveOffset(additionalOffset), func(innerG
Gomega, ctx context.Context) {
response, err := callSolrApiInPod(
ctx,
solrCloud,
@@ -476,6 +477,20 @@ func callSolrApiInPod(ctx context.Context, solrCloud
*solrv1beta1.SolrCloud, htt
queryParamsString = "?" + queryParamsString
}
+ toolOpts := ""
+ if solrCloud.Spec.SolrSecurity != nil &&
solrCloud.Spec.SolrSecurity.AuthenticationType == solrv1beta1.Basic {
+ basicAuthSecretName := solrCloud.BasicAuthSecretName()
+ basicAuthSecret := &corev1.Secret{}
+ if err = k8sClient.Get(ctx, resourceKey(solrCloud,
basicAuthSecretName), basicAuthSecret); err != nil {
+ return "", err
+ }
+ toolOpts =
+ "JAVA_TOOL_OPTIONS=\"-Dbasicauth=" +
+
string(basicAuthSecret.Data[corev1.BasicAuthUsernameKey]) + ":" +
string(basicAuthSecret.Data[corev1.BasicAuthPasswordKey]) +
+ "
-Dsolr.httpclient.builder.factory=org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory\""
+ }
+ GinkgoLogr.Info(toolOpts)
+
command := []string{
"solr",
"api",
@@ -489,6 +504,12 @@ func callSolrApiInPod(ctx context.Context, solrCloud
*solrv1beta1.SolrCloud, htt
apiPath,
queryParamsString),
}
+ if toolOpts != "" {
+ commandString := fmt.Sprintf("%s %s", toolOpts,
strings.Join(command, " "))
+ commandString =
regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(commandString), "
")
+
+ command = []string{"sh", "-c", fmt.Sprintf("%q", commandString)}
+ }
return runExecForContainer(ctx, util.SolrNodeContainer,
solrCloud.GetRandomSolrPodName(), solrCloud.Namespace, command)
}
@@ -655,17 +676,6 @@ func generateBaseSolrCloudWithSecurityJSON(replicas int)
*solrv1beta1.SolrCloud
solrCloud.Spec.SolrSecurity = &solrv1beta1.SolrSecurityOptions{}
}
- solrCloud.Spec.SolrSecurity.BootstrapSecurityJson =
&corev1.SecretKeySelector{
- LocalObjectReference: corev1.LocalObjectReference{
- Name: solrCloud.Name + "-security-secret",
- },
- Key: "security.json",
- }
-
- solrCloud.Spec.SolrSecurity.AuthenticationType = "Basic"
-
- solrCloud.Spec.SolrSecurity.BasicAuthSecret = solrCloud.Name +
"-basic-auth-secret"
-
return solrCloud
}
diff --git a/tests/scripts/manage_e2e_tests.sh
b/tests/scripts/manage_e2e_tests.sh
index 9af0c5b..2da32c4 100755
--- a/tests/scripts/manage_e2e_tests.sh
+++ b/tests/scripts/manage_e2e_tests.sh
@@ -76,7 +76,7 @@ if [[ -z "${KUBERNETES_VERSION:-}" ]]; then
KUBERNETES_VERSION="v1.26.6"
fi
if [[ -z "${SOLR_IMAGE:-}" ]]; then
- SOLR_IMAGE="${SOLR_VERSION:-9.4.0}"
+ SOLR_IMAGE="${SOLR_VERSION:-9.8.1}"
fi
if [[ "${SOLR_IMAGE}" != *":"* ]]; then
SOLR_IMAGE="solr:${SOLR_IMAGE}"