Skip to content

Commit fb91f9d

Browse files
committed
Add default permission for replica balancing (#770)
"collection-admin-edit" for Solr 9.9+ and "/____v2/cluster/replicas/balance" for previous versions (cherry picked from commit 494471b)
1 parent fc63dd3 commit fb91f9d

File tree

7 files changed

+141
-25
lines changed

7 files changed

+141
-25
lines changed

controllers/util/solr_security_util.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ func reconcileForBasicAuthWithBootstrappedSecurityJson(ctx context.Context, clie
117117

118118
// supply the bootstrap security.json to the initContainer via a simple BASE64 encoding env var
119119
security.SecurityJson = string(bootstrapSecret.Data[SecurityJsonFile])
120+
security.SecurityJsonSrc = &corev1.EnvVarSource{
121+
SecretKeyRef: &corev1.SecretKeySelector{
122+
LocalObjectReference: corev1.LocalObjectReference{Name: bootstrapSecret.Name}, Key: SecurityJsonFile}}
120123
basicAuthSecret = authSecret
121124
}
122125

@@ -393,7 +396,9 @@ func generateSecurityJson(solrCloud *solr.SolrCloud) map[string][]byte {
393396
{ "name": "k8s-metrics", "role":"k8s", "collection": null, "path":"/admin/metrics" },
394397
{ "name": "k8s-zk", "role":"k8s", "collection": null, "path":"/admin/zookeeper/status" },
395398
{ "name": "k8s-ping", "role":"k8s", "collection": "*", "path":"/admin/ping" },
396-
{ "name": "read", "role":["admin","users"] },
399+
{ "name": "k8s-replica-balancing", "role":"k8s", "collection": null, "path":"/____v2/cluster/replicas/balance" },
400+
{ "name": "collection-admin-edit", "role":"k8s" },
401+
{ "name": "read", "role":["admin","users","k8s"] },
397402
{ "name": "update", "role":["admin"] },
398403
{ "name": "security-read", "role": ["admin"] },
399404
{ "name": "security-edit", "role": ["admin"] },

docs/solr-cloud/solr-cloud-crd.md

+16
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,16 @@ Take a moment to review these authorization rules so that you're aware of the ro
10321032
"collection": "*",
10331033
"path": "/admin/ping"
10341034
},
1035+
{
1036+
"name": "k8s-replica-balancing",
1037+
"role": "k8s",
1038+
"collection": null,
1039+
"path": "/____v2/cluster/replicas/balance"
1040+
},
1041+
{
1042+
"name": "collection-admin-edit",
1043+
"role": "k8s"
1044+
},
10351045
{
10361046
"name": "read",
10371047
"role": [ "admin", "users" ]
@@ -1165,6 +1175,12 @@ Users need to ensure their `security.json` contains the user supplied in the `ba
11651175
/admin/metrics
11661176
/admin/ping (for collection="*")
11671177
/admin/zookeeper/status
1178+
/____v2/cluster/replicas/balance
1179+
```
1180+
1181+
And the following named permissions:
1182+
```aiignore
1183+
collection-admin-edit
11681184
```
11691185
_Tip: see the authorization rules defined by the default `security.json` as a guide for configuring access for the operator user_
11701186

helm/solr-operator/Chart.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ annotations:
7878
url: https://github.com/apache/solr-operator/issues/759
7979
- name: Github PR
8080
url: https://github.com/apache/solr-operator/pull/769
81+
- kind: fixed
82+
description: "Fix default bootstrapped permissions for replica balancing"
83+
links:
84+
- name: Github Issue
85+
url: https://github.com/apache/solr-operator/issues/653
86+
- name: Github PR
87+
url: https://github.com/apache/solr-operator/pull/770
8188
artifacthub.io/images: |
8289
- name: solr-operator
8390
image: apache/solr-operator:v0.9.1-prerelease

tests/e2e/solrcloud_security_json_test.go

+52-7
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ package e2e
2020
import (
2121
"context"
2222
solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
23+
"github.com/apache/solr-operator/controllers"
2324
. "github.com/onsi/ginkgo/v2"
2425
. "github.com/onsi/gomega"
26+
appsv1 "k8s.io/api/apps/v1"
27+
corev1 "k8s.io/api/core/v1"
2528
"k8s.io/utils/pointer"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
30+
"time"
2631
)
2732

2833
var _ = FDescribe("E2E - SolrCloud - Security JSON", func() {
@@ -35,10 +40,6 @@ var _ = FDescribe("E2E - SolrCloud - Security JSON", func() {
3540
})
3641

3742
JustBeforeEach(func(ctx context.Context) {
38-
By("generating the security.json secret and basic auth secret")
39-
generateSolrSecuritySecret(ctx, solrCloud)
40-
generateSolrBasicAuthSecret(ctx, solrCloud)
41-
4243
By("creating the SolrCloud")
4344
Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())
4445

@@ -50,20 +51,64 @@ var _ = FDescribe("E2E - SolrCloud - Security JSON", func() {
5051
solrCloud = expectSolrCloudToBeReady(ctx, solrCloud)
5152

5253
By("creating a first Solr Collection")
53-
createAndQueryCollection(ctx, solrCloud, "basic", 1, 1)
54+
createAndQueryCollection(ctx, solrCloud, "basic", 2, 1)
5455
})
5556

56-
FContext("Provided Zookeeper", func() {
57-
BeforeEach(func() {
57+
FContext("Provided Security JSON", func() {
58+
BeforeEach(func(ctx context.Context) {
5859
solrCloud.Spec.ZookeeperRef = &solrv1beta1.ZookeeperRef{
5960
ProvidedZookeeper: &solrv1beta1.ZookeeperSpec{
6061
Replicas: pointer.Int32(1),
6162
Ephemeral: &solrv1beta1.ZKEphemeral{},
6263
},
6364
}
65+
66+
solrCloud.Spec.SolrSecurity = &solrv1beta1.SolrSecurityOptions{
67+
AuthenticationType: "Basic",
68+
BasicAuthSecret: solrCloud.Name + "-basic-auth-secret",
69+
BootstrapSecurityJson: &corev1.SecretKeySelector{
70+
LocalObjectReference: corev1.LocalObjectReference{
71+
Name: solrCloud.Name + "-security-secret",
72+
},
73+
Key: "security.json",
74+
},
75+
}
76+
77+
By("generating the security.json secret and basic auth secret")
78+
generateSolrSecuritySecret(ctx, solrCloud)
79+
generateSolrBasicAuthSecret(ctx, solrCloud)
6480
})
6581

6682
// All testing will be done in the "JustBeforeEach" logic, no additional tests required here
6783
FIt("Starts correctly", func(ctx context.Context) {})
6884
})
85+
86+
FContext("Bootstrapped Security", func() {
87+
88+
BeforeEach(func() {
89+
solrCloud.Spec.SolrSecurity = &solrv1beta1.SolrSecurityOptions{
90+
AuthenticationType: "Basic",
91+
}
92+
})
93+
94+
FIt("Scales up with replica migration", func(ctx context.Context) {
95+
originalSolrCloud := solrCloud.DeepCopy()
96+
solrCloud.Spec.Replicas = pointer.Int32(int32(2))
97+
By("triggering a scale up via solrCloud replicas")
98+
Expect(k8sClient.Patch(ctx, solrCloud, client.MergeFrom(originalSolrCloud))).To(Succeed(), "Could not patch SolrCloud replicas to initiate scale down")
99+
100+
By("make sure scaleDown happens without a clusterLock and eventually the replicas are removed")
101+
// Once the scale down actually occurs, the statefulSet annotations should be removed very soon
102+
expectStatefulSetWithChecksAndTimeout(ctx, solrCloud, solrCloud.StatefulSetName(), time.Minute*2, time.Millisecond*500, func(g Gomega, found *appsv1.StatefulSet) {
103+
g.Expect(found.Spec.Replicas).To(HaveValue(BeEquivalentTo(2)), "StatefulSet should eventually have 2 pods.")
104+
clusterOp, err := controllers.GetCurrentClusterOp(found)
105+
g.Expect(err).ToNot(HaveOccurred(), "Error occurred while finding clusterLock for SolrCloud")
106+
g.Expect(clusterOp).To(BeNil(), "StatefulSet should have a ScaleDown lock after scaling is complete.")
107+
})
108+
109+
queryCollection(ctx, solrCloud, "basic", 0)
110+
111+
// TODO: When balancing is in all Operator supported Solr versions, add a test to make sure balancing occurred
112+
})
113+
})
69114
})

tests/e2e/suite_test.go

+34-1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ var _ = SynchronizedBeforeSuite(func(ctx context.Context) {
106106
var err error
107107
k8sConfig, err = config.GetConfig()
108108
Expect(err).NotTo(HaveOccurred(), "Could not load in default kubernetes config")
109+
k8sConfig.Timeout = time.Minute
109110
Expect(zkApi.AddToScheme(scheme.Scheme)).To(Succeed())
110111
Expect(certManagerApi.AddToScheme(scheme.Scheme)).To(Succeed())
111112
k8sClient, err = client.New(k8sConfig, client.Options{Scheme: scheme.Scheme})
@@ -338,14 +339,33 @@ func writeAllSolrInfoToFiles(ctx context.Context, directory string, namespace st
338339
}
339340

340341
foundServices := &corev1.ServiceList{}
341-
Expect(k8sClient.List(ctx, foundServices, listOps)).To(Succeed(), "Could not fetch Solr pods")
342+
Expect(k8sClient.List(ctx, foundServices, listOps)).To(Succeed(), "Could not fetch Solr services")
342343
Expect(foundServices).ToNot(BeNil(), "No Solr services could be found")
343344
for _, service := range foundServices.Items {
344345
writeAllServiceInfoToFiles(
345346
directory+service.Name+".service",
346347
&service,
347348
)
348349
}
350+
351+
// Unfortunately the secrets don't have a technology label
352+
req, err = labels.NewRequirement("solr-cloud", selection.Exists, make([]string, 0))
353+
Expect(err).ToNot(HaveOccurred())
354+
355+
labelSelector = labels.Everything().Add(*req)
356+
listOps = &client.ListOptions{
357+
Namespace: namespace,
358+
LabelSelector: labelSelector,
359+
}
360+
361+
foundSecrets := &corev1.SecretList{}
362+
Expect(k8sClient.List(ctx, foundSecrets, listOps)).To(Succeed(), "Could not fetch Solr secrets")
363+
for _, secret := range foundSecrets.Items {
364+
writeAllSecretInfoToFiles(
365+
directory+secret.Name+".secret",
366+
&secret,
367+
)
368+
}
349369
}
350370

351371
// 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) {
401421
Expect(writeErr).ToNot(HaveOccurred(), "Could not write service json to file")
402422
}
403423

424+
// writeAllSecretInfoToFiles writes the following each to a separate file with the given base name & directory.
425+
// - Service
426+
func writeAllSecretInfoToFiles(baseFilename string, secret *corev1.Secret) {
427+
// Write service to a file
428+
statusFile, err := os.Create(baseFilename + ".json")
429+
defer statusFile.Close()
430+
Expect(err).ToNot(HaveOccurred(), "Could not open file to save secret status: %s", baseFilename+".json")
431+
jsonBytes, marshErr := json.MarshalIndent(secret, "", "\t")
432+
Expect(marshErr).ToNot(HaveOccurred(), "Could not serialize secret json")
433+
_, writeErr := statusFile.Write(jsonBytes)
434+
Expect(writeErr).ToNot(HaveOccurred(), "Could not write secret json to file")
435+
}
436+
404437
// writeAllPodInfoToFile writes the following each to a separate file with the given base name & directory.
405438
// - Pod Spec/Status
406439
// - Pod Events

tests/e2e/test_utils_test.go

+25-15
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"fmt"
2626
"io"
2727
"os"
28+
"regexp"
2829
"strconv"
2930
"strings"
3031
"time"
@@ -233,7 +234,7 @@ func createAndQueryCollectionWithGomega(ctx context.Context, solrCloud *solrv1be
233234
}
234235

235236
additionalOffset += 1
236-
g.EventuallyWithOffset(additionalOffset, func(innerG Gomega) {
237+
g.EventuallyWithOffset(additionalOffset, func(innerG Gomega, ctx context.Context) {
237238
response, err := callSolrApiInPod(
238239
ctx,
239240
solrCloud,
@@ -246,7 +247,7 @@ func createAndQueryCollectionWithGomega(ctx context.Context, solrCloud *solrv1be
246247
}).Within(time.Second*10).WithContext(ctx).Should(Succeed(), "Collection creation command start was not successful")
247248
// Only wait 5 seconds when trying to create the asyncCommand
248249

249-
g.EventuallyWithOffset(additionalOffset, func(innerG Gomega) {
250+
g.EventuallyWithOffset(additionalOffset, func(innerG Gomega, ctx context.Context) {
250251
response, err := callSolrApiInPod(
251252
ctx,
252253
solrCloud,
@@ -271,7 +272,7 @@ func createAndQueryCollectionWithGomega(ctx context.Context, solrCloud *solrv1be
271272
innerG.Expect(response).To(ContainSubstring("\"state\":\"completed\""), "Did not finish creating Solr Collection in time")
272273
}).Within(time.Second*40).WithContext(ctx).Should(Succeed(), "Collection creation was not successful")
273274

274-
g.EventuallyWithOffset(additionalOffset, func(innerG Gomega) {
275+
g.EventuallyWithOffset(additionalOffset, func(innerG Gomega, ctx context.Context) {
275276
response, err := callSolrApiInPod(
276277
ctx,
277278
solrCloud,
@@ -296,7 +297,7 @@ func queryCollection(ctx context.Context, solrCloud *solrv1beta1.SolrCloud, coll
296297
}
297298

298299
func queryCollectionWithGomega(ctx context.Context, solrCloud *solrv1beta1.SolrCloud, collection string, docCount int, g Gomega, additionalOffset ...int) {
299-
g.EventuallyWithOffset(resolveOffset(additionalOffset), func(innerG Gomega) {
300+
g.EventuallyWithOffset(resolveOffset(additionalOffset), func(innerG Gomega, ctx context.Context) {
300301
response, err := callSolrApiInPod(
301302
ctx,
302303
solrCloud,
@@ -476,6 +477,20 @@ func callSolrApiInPod(ctx context.Context, solrCloud *solrv1beta1.SolrCloud, htt
476477
queryParamsString = "?" + queryParamsString
477478
}
478479

480+
toolOpts := ""
481+
if solrCloud.Spec.SolrSecurity != nil && solrCloud.Spec.SolrSecurity.AuthenticationType == solrv1beta1.Basic {
482+
basicAuthSecretName := solrCloud.BasicAuthSecretName()
483+
basicAuthSecret := &corev1.Secret{}
484+
if err = k8sClient.Get(ctx, resourceKey(solrCloud, basicAuthSecretName), basicAuthSecret); err != nil {
485+
return "", err
486+
}
487+
toolOpts =
488+
"JAVA_TOOL_OPTIONS=\"-Dbasicauth=" +
489+
string(basicAuthSecret.Data[corev1.BasicAuthUsernameKey]) + ":" + string(basicAuthSecret.Data[corev1.BasicAuthPasswordKey]) +
490+
" -Dsolr.httpclient.builder.factory=org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory\""
491+
}
492+
GinkgoLogr.Info(toolOpts)
493+
479494
command := []string{
480495
"solr",
481496
"api",
@@ -489,6 +504,12 @@ func callSolrApiInPod(ctx context.Context, solrCloud *solrv1beta1.SolrCloud, htt
489504
apiPath,
490505
queryParamsString),
491506
}
507+
if toolOpts != "" {
508+
commandString := fmt.Sprintf("%s %s", toolOpts, strings.Join(command, " "))
509+
commandString = regexp.MustCompile(`\s+`).ReplaceAllString(strings.TrimSpace(commandString), " ")
510+
511+
command = []string{"sh", "-c", fmt.Sprintf("%q", commandString)}
512+
}
492513
return runExecForContainer(ctx, util.SolrNodeContainer, solrCloud.GetRandomSolrPodName(), solrCloud.Namespace, command)
493514
}
494515

@@ -655,17 +676,6 @@ func generateBaseSolrCloudWithSecurityJSON(replicas int) *solrv1beta1.SolrCloud
655676
solrCloud.Spec.SolrSecurity = &solrv1beta1.SolrSecurityOptions{}
656677
}
657678

658-
solrCloud.Spec.SolrSecurity.BootstrapSecurityJson = &corev1.SecretKeySelector{
659-
LocalObjectReference: corev1.LocalObjectReference{
660-
Name: solrCloud.Name + "-security-secret",
661-
},
662-
Key: "security.json",
663-
}
664-
665-
solrCloud.Spec.SolrSecurity.AuthenticationType = "Basic"
666-
667-
solrCloud.Spec.SolrSecurity.BasicAuthSecret = solrCloud.Name + "-basic-auth-secret"
668-
669679
return solrCloud
670680
}
671681

tests/scripts/manage_e2e_tests.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ if [[ -z "${KUBERNETES_VERSION:-}" ]]; then
7676
KUBERNETES_VERSION="v1.26.6"
7777
fi
7878
if [[ -z "${SOLR_IMAGE:-}" ]]; then
79-
SOLR_IMAGE="${SOLR_VERSION:-9.4.0}"
79+
SOLR_IMAGE="${SOLR_VERSION:-9.8.1}"
8080
fi
8181
if [[ "${SOLR_IMAGE}" != *":"* ]]; then
8282
SOLR_IMAGE="solr:${SOLR_IMAGE}"

0 commit comments

Comments
 (0)