This is an automated email from the ASF dual-hosted git repository.
thelabdude 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 505e244 Support to bootstrap from a user-supplied security.json and
minor refactorings to pass auth headers through a Context (#356)
505e244 is described below
commit 505e244c5ae74a2f49565623028e7d5195d7eed7
Author: Timothy Potter <[email protected]>
AuthorDate: Fri Oct 29 12:42:26 2021 -0600
Support to bootstrap from a user-supplied security.json and minor
refactorings to pass auth headers through a Context (#356)
---
api/v1beta1/solrcloud_types.go | 6 +
api/v1beta1/zz_generated.deepcopy.go | 7 +-
config/crd/bases/solr.apache.org_solrclouds.yaml | 15 ++
controllers/solrbackup_controller.go | 18 +-
controllers/solrcloud_controller.go | 9 +-
.../solrcloud_controller_basic_auth_test.go | 103 +++++++--
controllers/util/backup_util.go | 13 +-
controllers/util/solr_api/api.go | 11 +-
controllers/util/solr_security_util.go | 253 +++++++++++++--------
controllers/util/solr_update_util.go | 7 +-
controllers/util/solr_util.go | 6 +-
docs/solr-cloud/solr-cloud-crd.md | 53 +++--
helm/solr-operator/Chart.yaml | 7 +
helm/solr-operator/crds/crds.yaml | 15 ++
helm/solr/README.md | 2 +
helm/solr/values.yaml | 3 +
16 files changed, 376 insertions(+), 152 deletions(-)
diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index fb83951..e61aa76 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -1459,4 +1459,10 @@ type SolrSecurityOptions struct {
// endpoints with credentials sourced from an env var instead of HTTP
directly.
// +optional
ProbesRequireAuth bool `json:"probesRequireAuth,omitempty"`
+
+ // Configure a user-provided security.json from a secret to allow for
advanced security config.
+ // If not specified, the operator bootstraps a security.json with basic
auth enabled.
+ // This is a bootstrapping config only; once Solr is initialized, the
security config should be managed by the security API.
+ // +optional
+ BootstrapSecurityJson *corev1.SecretKeySelector
`json:"bootstrapSecurityJson,omitempty"`
}
diff --git a/api/v1beta1/zz_generated.deepcopy.go
b/api/v1beta1/zz_generated.deepcopy.go
index 01ef438..a581e09 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -970,7 +970,7 @@ func (in *SolrCloudSpec) DeepCopyInto(out *SolrCloudSpec) {
if in.SolrSecurity != nil {
in, out := &in.SolrSecurity, &out.SolrSecurity
*out = new(SolrSecurityOptions)
- **out = **in
+ (*in).DeepCopyInto(*out)
}
if in.BackupRepositories != nil {
in, out := &in.BackupRepositories, &out.BackupRepositories
@@ -1247,6 +1247,11 @@ func (in *SolrReference) DeepCopy() *SolrReference {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver,
writing into out. in must be non-nil.
func (in *SolrSecurityOptions) DeepCopyInto(out *SolrSecurityOptions) {
*out = *in
+ if in.BootstrapSecurityJson != nil {
+ in, out := &in.BootstrapSecurityJson, &out.BootstrapSecurityJson
+ *out = new(v1.SecretKeySelector)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver,
creating a new SolrSecurityOptions.
diff --git a/config/crd/bases/solr.apache.org_solrclouds.yaml
b/config/crd/bases/solr.apache.org_solrclouds.yaml
index dc363fd..7fff8bd 100644
--- a/config/crd/bases/solr.apache.org_solrclouds.yaml
+++ b/config/crd/bases/solr.apache.org_solrclouds.yaml
@@ -5895,6 +5895,21 @@ spec:
basicAuthSecret:
description: "Secret (kubernetes.io/basic-auth) containing
credentials the operator should use for API requests to secure Solr pods. If
you provide this secret, then the operator assumes you've also configured your
own security.json file and uploaded it to Solr. If you change the password for
this user using the Solr security API, then you *must* update the secret with
the new password or the operator will be locked out of Solr and API requests
will fail, ultimately [...]
type: string
+ bootstrapSecurityJson:
+ description: Configure a user-provided security.json from
a secret to allow for advanced security config. If not specified, the operator
bootstraps a security.json with basic auth enabled. This is a bootstrapping
config only; once Solr is initialized, the security config should be managed by
the security API.
+ properties:
+ key:
+ description: The key of the secret to select from.
Must be a valid secret key.
+ type: string
+ name:
+ description: 'Name of the referent. More info:
https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
+ type: string
+ optional:
+ description: Specify whether the Secret or its key
must be defined
+ type: boolean
+ required:
+ - key
+ type: object
probesRequireAuth:
description: Flag to indicate if the configured HTTP
endpoint(s) used for the probes require authentication; defaults to false. If
you set to true, then probes will use a local command on the main container to
hit the secured endpoints with credentials sourced from an env var instead of
HTTP directly.
type: boolean
diff --git a/controllers/solrbackup_controller.go
b/controllers/solrbackup_controller.go
index 228036b..77373e3 100644
--- a/controllers/solrbackup_controller.go
+++ b/controllers/solrbackup_controller.go
@@ -26,7 +26,6 @@ import (
"github.com/apache/solr-operator/controllers/util"
"github.com/go-logr/logr"
batchv1 "k8s.io/api/batch/v1"
- corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -160,13 +159,12 @@ func (r *SolrBackupReconciler)
reconcileSolrCloudBackup(ctx context.Context, bac
return nil, collectionBackupsFinished, actionTaken, err
}
- var httpHeaders map[string]string
+ // Add any additional values needed to Authn to Solr to the Context
used when invoking the API
if solrCloud.Spec.SolrSecurity != nil {
- basicAuthSecret := &corev1.Secret{}
- if err := r.Get(ctx, types.NamespacedName{Name:
solrCloud.BasicAuthSecretName(), Namespace: solrCloud.Namespace},
basicAuthSecret); err != nil {
+ ctx, err = util.AddAuthToContext(ctx, &r.Client,
solrCloud.Spec.SolrSecurity, solrCloud.Namespace)
+ if err != nil {
return nil, collectionBackupsFinished, actionTaken, err
}
- httpHeaders = map[string]string{"Authorization":
util.BasicAuthHeader(basicAuthSecret)}
}
// First check if the collection backups have been completed
@@ -208,7 +206,7 @@ func (r *SolrBackupReconciler) reconcileSolrCloudBackup(ctx
context.Context, bac
// Go through each collection specified and reconcile the backup.
for _, collection := range backup.Spec.Collections {
- _, err = reconcileSolrCollectionBackup(backup, solrCloud,
backupRepository, collection, httpHeaders, logger)
+ _, err = reconcileSolrCollectionBackup(ctx, backup, solrCloud,
backupRepository, collection, logger)
}
// First check if the collection backups have been completed
@@ -217,7 +215,7 @@ func (r *SolrBackupReconciler) reconcileSolrCloudBackup(ctx
context.Context, bac
return solrCloud, collectionBackupsFinished, actionTaken, err
}
-func reconcileSolrCollectionBackup(backup *solrv1beta1.SolrBackup, solrCloud
*solrv1beta1.SolrCloud, backupRepository *solrv1beta1.SolrBackupRepository,
collection string, httpHeaders map[string]string, logger logr.Logger) (finished
bool, err error) {
+func reconcileSolrCollectionBackup(ctx context.Context, backup
*solrv1beta1.SolrBackup, solrCloud *solrv1beta1.SolrCloud, backupRepository
*solrv1beta1.SolrBackupRepository, collection string, logger logr.Logger)
(finished bool, err error) {
now := metav1.Now()
collectionBackupStatus := solrv1beta1.CollectionBackupStatus{}
collectionBackupStatus.Collection = collection
@@ -233,7 +231,7 @@ func reconcileSolrCollectionBackup(backup
*solrv1beta1.SolrBackup, solrCloud *so
// If the collection backup hasn't started, start it
if !collectionBackupStatus.InProgress &&
!collectionBackupStatus.Finished {
// Start the backup by calling solr
- started, err := util.StartBackupForCollection(solrCloud,
backupRepository, backup, collection, httpHeaders, logger)
+ started, err := util.StartBackupForCollection(ctx, solrCloud,
backupRepository, backup, collection, logger)
if err != nil {
return true, err
}
@@ -244,7 +242,7 @@ func reconcileSolrCollectionBackup(backup
*solrv1beta1.SolrBackup, solrCloud *so
collectionBackupStatus.BackupName =
util.FullCollectionBackupName(collection, backup.Name)
} else if collectionBackupStatus.InProgress {
// Check the state of the backup, when it is in progress, and
update the state accordingly
- finished, successful, asyncStatus, err :=
util.CheckBackupForCollection(solrCloud, collection, backup.Name, httpHeaders,
logger)
+ finished, successful, asyncStatus, err :=
util.CheckBackupForCollection(ctx, solrCloud, collection, backup.Name, logger)
if err != nil {
return false, err
}
@@ -259,7 +257,7 @@ func reconcileSolrCollectionBackup(backup
*solrv1beta1.SolrBackup, solrCloud *so
collectionBackupStatus.FinishTime = &now
}
- err = util.DeleteAsyncInfoForBackup(solrCloud,
collection, backup.Name, httpHeaders, logger)
+ err = util.DeleteAsyncInfoForBackup(ctx, solrCloud,
collection, backup.Name, logger)
} else {
collectionBackupStatus.AsyncBackupStatus = asyncStatus
}
diff --git a/controllers/solrcloud_controller.go
b/controllers/solrcloud_controller.go
index a5875ae..b854855 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -408,14 +408,17 @@ func (r *SolrCloudReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
}
// If authn enabled on Solr, we need to pass the auth header
- var authHeader map[string]string
if security != nil {
- authHeader = security.AuthHeader()
+ ctx, err = security.AddAuthToContext(ctx)
+ if err != nil {
+ updateLogger.Error(err, "failed to create
Authorization header when reconciling", "SolrCloud", instance.Name)
+ return requeueOrNot, err
+ }
}
// Pick which pods should be deleted for an update.
// Don't exit on an error, which would only occur because of an
HTTP Exception. Requeue later instead.
- additionalPodsToUpdate, retryLater :=
util.DeterminePodsSafeToUpdate(instance, outOfDatePods,
int(newStatus.ReadyReplicas), availableUpdatedPodCount,
len(outOfDatePodsNotStarted), updateLogger, authHeader)
+ additionalPodsToUpdate, retryLater :=
util.DeterminePodsSafeToUpdate(ctx, instance, outOfDatePods,
int(newStatus.ReadyReplicas), availableUpdatedPodCount,
len(outOfDatePodsNotStarted), updateLogger)
podsToUpdate = append(podsToUpdate, additionalPodsToUpdate...)
for _, pod := range podsToUpdate {
diff --git a/controllers/solrcloud_controller_basic_auth_test.go
b/controllers/solrcloud_controller_basic_auth_test.go
index 433f67e..05594a5 100644
--- a/controllers/solrcloud_controller_basic_auth_test.go
+++ b/controllers/solrcloud_controller_basic_auth_test.go
@@ -27,6 +27,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/util/intstr"
)
var _ = FDescribe("SolrCloud controller - Basic Auth", func() {
@@ -82,6 +83,34 @@ var _ = FDescribe("SolrCloud controller - Basic Auth",
func() {
})
})
+ FContext("Boostrap Security JSON with Custom Probe Paths", func() {
+ BeforeEach(func() {
+ customHandler := corev1.Handler{
+ HTTPGet: &corev1.HTTPGetAction{
+ Scheme: corev1.URISchemeHTTP,
+ Path: "/solr/readyz",
+ Port: intstr.FromInt(8983),
+ },
+ }
+
+ // verify users can vary the probe path and the secure
probe exec command uses them
+ solrCloud.Spec.CustomSolrKubeOptions =
solrv1beta1.CustomSolrKubeOptions{
+ PodOptions: &solrv1beta1.PodOptions{
+ LivenessProbe: &corev1.Probe{Handler:
customHandler},
+ ReadinessProbe: &corev1.Probe{Handler:
customHandler},
+ },
+ }
+
+ solrCloud.Spec.SolrSecurity =
&solrv1beta1.SolrSecurityOptions{
+ AuthenticationType: solrv1beta1.Basic,
+ ProbesRequireAuth: true,
+ }
+ })
+ FIt("has the correct resources", func() {
+ expectStatefulSetBasicAuthConfig(ctx, solrCloud, true)
+ })
+ })
+
FContext("Boostrap Security JSON with ZK ACLs", func() {
BeforeEach(func() {
solrCloud.Spec.SolrSecurity =
&solrv1beta1.SolrSecurityOptions{
@@ -144,6 +173,34 @@ var _ = FDescribe("SolrCloud controller - Basic Auth",
func() {
expectStatefulSetBasicAuthConfig(ctx, solrCloud, false)
})
})
+
+ FContext("User Provided Credentials and security.json secret", func() {
+ BeforeEach(func() {
+ basicAuthSecretName := "my-basic-auth-secret"
+ solrCloud.Spec.SolrSecurity =
&solrv1beta1.SolrSecurityOptions{
+ AuthenticationType: solrv1beta1.Basic,
+ BasicAuthSecret: basicAuthSecretName,
+ BootstrapSecurityJson:
&corev1.SecretKeySelector{
+ LocalObjectReference:
corev1.LocalObjectReference{Name: "my-security-json"},
+ Key:
util.SecurityJsonFile,
+ },
+ }
+ })
+ FIt("has the correct resources", func() {
+ By("Making sure that no statefulSet exists until the
BasicAuth Secret is created")
+ expectNoStatefulSet(ctx, solrCloud,
solrCloud.StatefulSetName())
+
+ By("Create the basicAuth secret")
+ basicAuthSecret :=
createBasicAuthSecret(solrCloud.Spec.SolrSecurity.BasicAuthSecret,
solrv1beta1.DefaultBasicAuthUsername, solrCloud.Namespace)
+ Expect(k8sClient.Create(ctx,
basicAuthSecret)).To(Succeed(), "Could not create the necessary basicAuth
secret")
+
+ By("Create the security.json Secret")
+ createMockSecurityJsonSecret(ctx, "my-security-json",
solrCloud.Namespace)
+
+ By("Make sure the StatefulSet is created and configured
correctly")
+ expectStatefulSetBasicAuthConfig(ctx, solrCloud, false)
+ })
+ })
})
var boostrapedSecretKeys = []string{
@@ -155,8 +212,13 @@ var boostrapedSecretKeys = []string{
func expectStatefulSetBasicAuthConfig(ctx context.Context, sc
*solrv1beta1.SolrCloud, expectBootstrapSecret bool) *appsv1.StatefulSet {
Expect(sc.Spec.SolrSecurity).To(Not(BeNil()), "solrSecurity is not
configured for this SolrCloud instance!")
+ expProbePath := "/solr/admin/info/system"
+ if sc.Spec.CustomSolrKubeOptions.PodOptions != nil &&
sc.Spec.CustomSolrKubeOptions.PodOptions.LivenessProbe != nil {
+ expProbePath =
sc.Spec.CustomSolrKubeOptions.PodOptions.LivenessProbe.HTTPGet.Path
+ }
+
stateful := expectStatefulSetWithChecks(ctx, sc, sc.StatefulSetName(),
func(g Gomega, found *appsv1.StatefulSet) {
- expectBasicAuthConfigOnPodTemplateWithGomega(g, sc,
&found.Spec.Template, expectBootstrapSecret)
+ expectBasicAuthConfigOnPodTemplateWithGomega(g, sc,
&found.Spec.Template, expectBootstrapSecret, expProbePath)
})
expectSecretWithChecks(ctx, sc, sc.BasicAuthSecretName(), func(innerG
Gomega, found *corev1.Secret) {
@@ -192,12 +254,7 @@ func expectStatefulSetBasicAuthConfig(ctx context.Context,
sc *solrv1beta1.SolrC
}
// Ensures config is setup for basic-auth enabled Solr pods
-func expectBasicAuthConfigOnPodTemplate(solrCloud *solrv1beta1.SolrCloud,
podTemplate *corev1.PodTemplateSpec, expectBootstrapSecret bool)
*corev1.Container {
- return expectBasicAuthConfigOnPodTemplateWithGomega(Default, solrCloud,
podTemplate, expectBootstrapSecret)
-}
-
-// Ensures config is setup for basic-auth enabled Solr pods
-func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega, solrCloud
*solrv1beta1.SolrCloud, podTemplate *corev1.PodTemplateSpec,
expectBootstrapSecret bool) *corev1.Container {
+func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega, solrCloud
*solrv1beta1.SolrCloud, podTemplate *corev1.PodTemplateSpec,
expectBootstrapSecret bool, expProbePath string) *corev1.Container {
// check the env vars needed for the probes to work with auth
g.Expect(podTemplate.Spec.Containers).To(Not(BeEmpty()), "Solr Pod
requires containers")
mainContainer := podTemplate.Spec.Containers[0]
@@ -233,8 +290,8 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega,
solrCloud *solrv1bet
"-Dsolr.httpclient.builder.factory=org.apache.solr.client.solrj.impl.PreemptiveBasicAuthClientBuilderFactory
"+
"-Dsolr.install.dir=\"/opt/solr\"
-Dlog4j.configurationFile=\"/opt/solr/server/resources/log4j2-console.xml\" "+
"-classpath
\"/opt/solr/server/solr-webapp/webapp/WEB-INF/lib/*:/opt/solr/server/lib/ext/*:/opt/solr/server/lib/*\"
"+
- "org.apache.solr.util.SolrCLI api -get
http://localhost:8983/solr/admin/info/system",
- solrCloud.Name, solrCloud.Name)
+ "org.apache.solr.util.SolrCLI api -get
http://localhost:8983%s",
+ solrCloud.Name, solrCloud.Name, expProbePath)
g.Expect(mainContainer.LivenessProbe).To(Not(BeNil()), "main
container should have a liveness probe defined")
g.Expect(mainContainer.LivenessProbe.Exec).To(Not(BeNil()),
"liveness probe should have an exec when auth is enabled")
g.Expect(mainContainer.LivenessProbe.Exec.Command).To(Not(BeEmpty()), "liveness
probe command cannot be empty")
@@ -248,7 +305,7 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega,
solrCloud *solrv1bet
}
// if no user-provided auth secret, then check that security.json gets
bootstrapped correctly
- if solrCloud.Spec.SolrSecurity.BasicAuthSecret == "" {
+ if solrCloud.Spec.SolrSecurity.BasicAuthSecret == "" ||
solrCloud.Spec.SolrSecurity.BootstrapSecurityJson != nil {
// initContainers
g.Expect(podTemplate.Spec.InitContainers).To(Not(BeEmpty()),
"The Solr Pod template requires an init container to bootstrap the
security.json")
var expInitContainer *corev1.Container = nil
@@ -259,7 +316,7 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g Gomega,
solrCloud *solrv1bet
}
}
- if expectBootstrapSecret {
+ if expectBootstrapSecret ||
solrCloud.Spec.SolrSecurity.BootstrapSecurityJson != nil {
// if the zookeeperRef has ACLs set, verify the env
vars were set correctly for this initContainer
allACL, _ := solrCloud.Spec.ZookeeperRef.GetACLs()
if allACL != nil {
@@ -269,12 +326,7 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g
Gomega, solrCloud *solrv1bet
testACLEnvVarsWithGomega(g,
expInitContainer.Env[3:len(expInitContainer.Env)-2], true)
} // else this ref not using ACLs
- g.Expect(expInitContainer).To(Not(BeNil()), "Didn't
find the setup-zk InitContainer in the sts!")
- expCmd :=
"ZK_SECURITY_JSON=$(/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost
${ZK_HOST} -cmd get /security.json); " +
- "if [ ${#ZK_SECURITY_JSON} -lt 3 ]; then " +
- "echo $SECURITY_JSON > /tmp/security.json; " +
-
"/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost ${ZK_HOST} -cmd
putfile /security.json /tmp/security.json; echo \"put security.json in ZK\"; fi"
-
g.Expect(expInitContainer.Command[2]).To(ContainSubstring(expCmd), "setup-zk
initContainer not configured to bootstrap security.json!")
+ expectPutSecurityJsonInZkCmd(g, expInitContainer)
} else {
g.Expect(expInitContainer).To(Or(
BeNil(),
@@ -292,3 +344,20 @@ func expectBasicAuthConfigOnPodTemplateWithGomega(g
Gomega, solrCloud *solrv1bet
return &mainContainer // return as a convenience in case tests want to
do more checking on the main container
}
+
+func expectPutSecurityJsonInZkCmd(g Gomega, expInitContainer
*corev1.Container) {
+ g.Expect(expInitContainer).To(Not(BeNil()), "Didn't find the setup-zk
InitContainer in the sts!")
+ expCmd :=
"ZK_SECURITY_JSON=$(/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost
${ZK_HOST} -cmd get /security.json); " +
+ "if [ ${#ZK_SECURITY_JSON} -lt 3 ]; then " +
+ "echo $SECURITY_JSON > /tmp/security.json; " +
+ "/opt/solr/server/scripts/cloud-scripts/zkcli.sh -zkhost
${ZK_HOST} -cmd putfile /security.json /tmp/security.json; echo \"put
security.json in ZK\"; fi"
+ g.Expect(expInitContainer.Command[2]).To(ContainSubstring(expCmd),
"setup-zk initContainer not configured to bootstrap security.json!")
+}
+
+func createMockSecurityJsonSecret(ctx context.Context, name string, ns string)
corev1.Secret {
+ secData := map[string]string{}
+ secData[util.SecurityJsonFile] = "{}"
+ sec := corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: name,
Namespace: ns}, StringData: secData}
+ Expect(k8sClient.Create(ctx, &sec)).To(Succeed(), "Could not create
mock security.json secret")
+ return sec
+}
diff --git a/controllers/util/backup_util.go b/controllers/util/backup_util.go
index d0dd2d8..2670ca9 100644
--- a/controllers/util/backup_util.go
+++ b/controllers/util/backup_util.go
@@ -19,6 +19,7 @@ package util
import (
"bytes"
+ "context"
"fmt"
solr "github.com/apache/solr-operator/api/v1beta1"
"github.com/apache/solr-operator/controllers/util/solr_api"
@@ -320,12 +321,12 @@ func GenerateQueryParamsForBackup(backupRepository
*solr.SolrBackupRepository, b
return queryParams
}
-func StartBackupForCollection(cloud *solr.SolrCloud, backupRepository
*solr.SolrBackupRepository, backup *solr.SolrBackup, collection string,
httpHeaders map[string]string, logger logr.Logger) (success bool, err error) {
+func StartBackupForCollection(ctx context.Context, cloud *solr.SolrCloud,
backupRepository *solr.SolrBackupRepository, backup *solr.SolrBackup,
collection string, logger logr.Logger) (success bool, err error) {
queryParams := GenerateQueryParamsForBackup(backupRepository, backup,
collection)
resp := &solr_api.SolrAsyncResponse{}
logger.Info("Calling to start collection backup", "solrCloud",
cloud.Name, "collection", collection)
- err = solr_api.CallCollectionsApi(cloud, queryParams, httpHeaders, resp)
+ err = solr_api.CallCollectionsApi(ctx, cloud, queryParams, resp)
if err == nil {
if resp.ResponseHeader.Status == 0 {
@@ -338,7 +339,7 @@ func StartBackupForCollection(cloud *solr.SolrCloud,
backupRepository *solr.Solr
return success, err
}
-func CheckBackupForCollection(cloud *solr.SolrCloud, collection string,
backupName string, httpHeaders map[string]string, logger logr.Logger) (finished
bool, success bool, asyncStatus string, err error) {
+func CheckBackupForCollection(ctx context.Context, cloud *solr.SolrCloud,
collection string, backupName string, logger logr.Logger) (finished bool,
success bool, asyncStatus string, err error) {
queryParams := url.Values{}
queryParams.Add("action", "REQUESTSTATUS")
queryParams.Add("requestid", AsyncIdForCollectionBackup(collection,
backupName))
@@ -346,7 +347,7 @@ func CheckBackupForCollection(cloud *solr.SolrCloud,
collection string, backupNa
resp := &solr_api.SolrAsyncResponse{}
logger.Info("Calling to check on collection backup", "solrCloud",
cloud.Name, "collection", collection)
- err = solr_api.CallCollectionsApi(cloud, queryParams, httpHeaders, resp)
+ err = solr_api.CallCollectionsApi(ctx, cloud, queryParams, resp)
if err == nil {
if resp.ResponseHeader.Status == 0 {
@@ -367,7 +368,7 @@ func CheckBackupForCollection(cloud *solr.SolrCloud,
collection string, backupNa
return finished, success, asyncStatus, err
}
-func DeleteAsyncInfoForBackup(cloud *solr.SolrCloud, collection string,
backupName string, httpHeaders map[string]string, logger logr.Logger) (err
error) {
+func DeleteAsyncInfoForBackup(ctx context.Context, cloud *solr.SolrCloud,
collection string, backupName string, logger logr.Logger) (err error) {
queryParams := url.Values{}
queryParams.Add("action", "DELETESTATUS")
queryParams.Add("requestid", AsyncIdForCollectionBackup(collection,
backupName))
@@ -375,7 +376,7 @@ func DeleteAsyncInfoForBackup(cloud *solr.SolrCloud,
collection string, backupNa
resp := &solr_api.SolrAsyncResponse{}
logger.Info("Calling to delete async info for backup command.",
"solrCloud", cloud.Name, "collection", collection)
- err = solr_api.CallCollectionsApi(cloud, queryParams, httpHeaders, resp)
+ err = solr_api.CallCollectionsApi(ctx, cloud, queryParams, resp)
if err != nil {
logger.Error(err, "Error deleting async data for collection
backup", "solrCloud", cloud.Name, "collection", collection)
}
diff --git a/controllers/util/solr_api/api.go b/controllers/util/solr_api/api.go
index 8a6dd55..51ce412 100644
--- a/controllers/util/solr_api/api.go
+++ b/controllers/util/solr_api/api.go
@@ -18,6 +18,7 @@
package solr_api
import (
+ "context"
"crypto/tls"
"encoding/json"
"fmt"
@@ -28,6 +29,10 @@ import (
"net/url"
)
+const (
+ HTTP_HEADERS_CONTEXT_KEY = "HTTP_HEADERS"
+)
+
// Used to call a Solr pod over https when using a self-signed cert
// It's "insecure" but is only used for internal communication, such as
getting cluster status
// so if you're worried about this, don't use a self-signed cert
@@ -65,7 +70,7 @@ type SolrAsyncStatus struct {
Message string `json:"msg"`
}
-func CallCollectionsApi(cloud *solr.SolrCloud, urlParams url.Values,
httpHeaders map[string]string, response interface{}) (err error) {
+func CallCollectionsApi(ctx context.Context, cloud *solr.SolrCloud, urlParams
url.Values, response interface{}) (err error) {
cloudUrl := solr.InternalURLForCloud(cloud)
client := noVerifyTLSHttpClient
@@ -81,8 +86,8 @@ func CallCollectionsApi(cloud *solr.SolrCloud, urlParams
url.Values, httpHeaders
req, err := http.NewRequest("GET", cloudUrl, nil)
- // mainly for doing basic-auth
- if httpHeaders != nil {
+ // Any custom HTTP headers passed through the Context
+ if httpHeaders, hasHeaders :=
ctx.Value(HTTP_HEADERS_CONTEXT_KEY).(map[string]string); hasHeaders {
for key, header := range httpHeaders {
req.Header.Add(key, header)
}
diff --git a/controllers/util/solr_security_util.go
b/controllers/util/solr_security_util.go
index 9701ab2..c483181 100644
--- a/controllers/util/solr_security_util.go
+++ b/controllers/util/solr_security_util.go
@@ -24,6 +24,7 @@ import (
"encoding/json"
"fmt"
solr "github.com/apache/solr-operator/api/v1beta1"
+ "github.com/apache/solr-operator/controllers/util/solr_api"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
@@ -43,101 +44,134 @@ const (
DefaultProbePath = "/admin/info/system"
)
+// Utility struct holding security related config and objects resolved at
runtime needed during reconciliation,
+// such as the secret holding credentials the operator should use to make
calls to secure Solr
type SecurityConfig struct {
- BasicAuthSecret *corev1.Secret
- SecurityJson string
+ SolrSecurity *solr.SolrSecurityOptions
+ CredentialsSecret *corev1.Secret
+ SecurityJson string
+ SecurityJsonSrc *corev1.EnvVarSource
}
// Given a SolrCloud instance and an API service client, produce a
SecurityConfig needed to enable Solr security
func ReconcileSecurityConfig(ctx context.Context, client *client.Client,
instance *solr.SolrCloud) (*SecurityConfig, error) {
- reader := *client
+ sec := instance.Spec.SolrSecurity
+ if sec.AuthenticationType == solr.Basic {
+ return reconcileForBasicAuth(ctx, client, instance)
+ }
- security := &SecurityConfig{}
- basicAuthSecret := &corev1.Secret{}
+ // shouldn't ever get here since the YAML would be validated against
the enum before this, but keeping it here for human readers to grok the overall
flow
+ return nil, fmt.Errorf("%s not supported! Only 'Basic' authentication
is supported by the Solr operator", sec.AuthenticationType)
+}
+// Reconcile the credentials and supporting config needed to make calls to
Solr secured with basic auth
+// Also, bootstraps an initial security.json config if not supplied by the user
+// However, if users provide their own security.json, then they must also
provide the basic auth secret containing
+// credentials the operator should use for making calls to Solr. In other
words, we don't try to infuse a new user into
+// the user-provided security.json as that could get messy.
+func reconcileForBasicAuth(ctx context.Context, client *client.Client,
instance *solr.SolrCloud) (*SecurityConfig, error) {
// user has the option of providing a secret with credentials the
operator should use to make requests to Solr
+ if instance.Spec.SolrSecurity.BasicAuthSecret != "" {
+ return reconcileForBasicAuthWithUserProvidedSecret(ctx, client,
instance)
+ } else {
+ // user didn't provide a basicAuthSecret, so it's invalid for
them to provide a security.json as the operator
+ // has no way of authenticating to Solr with a user provided
security.json w/o also having the credentials in a secret
+ if instance.Spec.SolrSecurity.BootstrapSecurityJson != nil {
+ return nil, fmt.Errorf("invalid basic auth config, you
must also provide the 'basicAuthSecret' when providing your own
'security.json'")
+ }
+ return reconcileForBasicAuthWithBootstrappedSecurityJson(ctx,
client, instance)
+ }
+}
+
+// Create a "bootstrap" security.json with basic auth enabled with the
"admin", "solr", and "k8s" users having random passwords
+func reconcileForBasicAuthWithBootstrappedSecurityJson(ctx context.Context,
client *client.Client, instance *solr.SolrCloud) (*SecurityConfig, error) {
+ reader := *client
+
sec := instance.Spec.SolrSecurity
+ security := &SecurityConfig{SolrSecurity: sec}
- if sec.AuthenticationType != solr.Basic {
- return nil, fmt.Errorf("%s not supported! Only 'Basic'
authentication is supported by the Solr operator",
- instance.Spec.SolrSecurity.AuthenticationType)
- }
+ // We're supplying a secret with random passwords and a default
security.json
+ // since we randomly generate the passwords, we need to lookup the
secret first and only create if not exist
+ basicAuthSecret := &corev1.Secret{}
+ err := reader.Get(ctx, types.NamespacedName{Name:
instance.BasicAuthSecretName(), Namespace: instance.Namespace}, basicAuthSecret)
+ if err != nil && errors.IsNotFound(err) {
+ authSecret, bootstrapSecret :=
generateBasicAuthSecretWithBootstrap(instance)
- // TODO: we shouldn't need to enforce this restriction?!?
- //
- // for now, we don't support 'solrSecurity.probesRequireAuth=true' and
custom probe paths,
- // so make the user fix that so there are no surprises later
- if sec.ProbesRequireAuth &&
instance.Spec.CustomSolrKubeOptions.PodOptions != nil {
- for _, path := range GetCustomProbePaths(instance) {
- if path != DefaultProbePath {
- return nil, fmt.Errorf(
- "custom probe path %s not supported
when 'solrSecurity.probesRequireAuth=true'; must use
'solrSecurity.probesRequireAuth=false' when using custom probe endpoints", path)
- }
+ // take ownership of these secrets since we created them
+ if err := controllerutil.SetControllerReference(instance,
authSecret, reader.Scheme()); err != nil {
+ return nil, err
}
- }
-
- if sec.BasicAuthSecret != "" {
- // the user supplied their own basic auth secret, make sure it
exists and has the expected keys
- if err := reader.Get(ctx, types.NamespacedName{Name:
sec.BasicAuthSecret, Namespace: instance.Namespace}, basicAuthSecret); err !=
nil {
+ if err := controllerutil.SetControllerReference(instance,
bootstrapSecret, reader.Scheme()); err != nil {
return nil, err
}
-
- err := ValidateBasicAuthSecret(basicAuthSecret)
+ err = reader.Create(ctx, authSecret)
+ if err != nil {
+ return nil, err
+ }
+ err = reader.Create(ctx, bootstrapSecret)
if err != nil {
return nil, err
}
- // since the user supplied us with a basic auth secret, we're
assuming they're also bootstrapping the security.json,
- // so there is no bootstrap secret in this case
+ // supply the bootstrap security.json to the initContainer via
a simple BASE64 encoding env var
+ security.SecurityJson =
string(bootstrapSecret.Data[SecurityJsonFile])
+ basicAuthSecret = authSecret
+ }
- } else {
- // We're supplying a secret with random passwords and a default
security.json
- // since we randomly generate the passwords, we need to lookup
the secret first and only create if not exist
- err := reader.Get(ctx, types.NamespacedName{Name:
instance.BasicAuthSecretName(), Namespace: instance.Namespace}, basicAuthSecret)
- if err != nil && errors.IsNotFound(err) {
- authSecret, bootstrapSecret :=
generateBasicAuthSecretWithBootstrap(instance)
-
- // take ownership of these secrets since we created them
- if err :=
controllerutil.SetControllerReference(instance, authSecret, reader.Scheme());
err != nil {
- return nil, err
- }
- if err :=
controllerutil.SetControllerReference(instance, bootstrapSecret,
reader.Scheme()); err != nil {
- return nil, err
- }
- err = reader.Create(ctx, authSecret)
- if err != nil {
- return nil, err
- }
- err = reader.Create(ctx, bootstrapSecret)
- if err != nil {
- return nil, err
- }
+ if err != nil {
+ return nil, err
+ }
+ security.CredentialsSecret = basicAuthSecret
- // supply the bootstrap security.json to the
initContainer via a simple BASE64 encoding env var
+ if security.SecurityJson == "" {
+ // the bootstrap secret already exists, so just stash the
security.json needed for constructing initContainers
+ bootstrapSecret := &corev1.Secret{}
+ err = reader.Get(ctx, types.NamespacedName{Name:
instance.SecurityBootstrapSecretName(), Namespace: instance.Namespace},
bootstrapSecret)
+ if err != nil {
+ if !errors.IsNotFound(err) {
+ return nil, err
+ } // else perhaps the user deleted it after security
was bootstrapped ... this is ok but may trigger a restart on the STS
+ } else {
+ // stash this so we can configure the setup-zk
initContainer to bootstrap the security.json in ZK
security.SecurityJson =
string(bootstrapSecret.Data[SecurityJsonFile])
- basicAuthSecret = authSecret
+ security.SecurityJsonSrc = &corev1.EnvVarSource{
+ SecretKeyRef: &corev1.SecretKeySelector{
+ LocalObjectReference:
corev1.LocalObjectReference{Name: bootstrapSecret.Name}, Key: SecurityJsonFile}}
}
+ }
+
+ return security, nil
+}
+
+// Basic auth but the user provides a secret containing credentials the
operator should use to make requests to a secure Solr
+func reconcileForBasicAuthWithUserProvidedSecret(ctx context.Context, client
*client.Client, instance *solr.SolrCloud) (*SecurityConfig, error) {
+ reader := *client
+
+ sec := instance.Spec.SolrSecurity
+ security := &SecurityConfig{SolrSecurity: sec}
+ // the user supplied their own basic auth secret, make sure it exists
and has the expected keys
+ basicAuthSecret := &corev1.Secret{}
+ if err := reader.Get(ctx, types.NamespacedName{Name:
sec.BasicAuthSecret, Namespace: instance.Namespace}, basicAuthSecret); err !=
nil {
+ return nil, err
+ }
+
+ err := ValidateBasicAuthSecret(basicAuthSecret)
+ if err != nil {
+ return nil, err
+ }
+ security.CredentialsSecret = basicAuthSecret
+
+ // is there a user-provided security.json in a secret?
+ // in this config, we don't need to enforce the user providing a
security.json as they can bootstrap the security.json however they want
+ if sec.BootstrapSecurityJson != nil {
+ securityJson, err := loadSecurityJsonFromSecret(ctx, client,
sec.BootstrapSecurityJson, instance.Namespace)
if err != nil {
return nil, err
}
-
- security.BasicAuthSecret = basicAuthSecret
-
- if security.SecurityJson == "" {
- // the bootstrap secret already exists, so just stash
the security.json needed for constructing initContainers
- bootstrapSecret := &corev1.Secret{}
- err = reader.Get(ctx, types.NamespacedName{Name:
instance.SecurityBootstrapSecretName(), Namespace: instance.Namespace},
bootstrapSecret)
- if err != nil {
- if !errors.IsNotFound(err) {
- return nil, err
- } // else perhaps the user deleted it after
security was bootstrapped ... this is ok but may trigger a restart on the STS
- } else {
- // stash this so we can configure the setup-zk
initContainer to bootstrap the security.json in ZK
- security.SecurityJson =
string(bootstrapSecret.Data[SecurityJsonFile])
- }
- }
- }
+ security.SecurityJson = securityJson
+ security.SecurityJsonSrc = &corev1.EnvVarSource{SecretKeyRef:
sec.BootstrapSecurityJson}
+ } // else no user-provided secret, no sweat for us
return security, nil
}
@@ -146,8 +180,9 @@ func enableSecureProbesOnSolrCloudStatefulSet(solrCloud
*solr.SolrCloud, statefu
mainContainer := &stateful.Spec.Template.Spec.Containers[0]
// if probes require auth or Solr wants client auth (mTLS), need to
invoke a command on the Solr pod for the probes
+ // but only Basic auth is supported for now
mountPath := ""
- if solrCloud.Spec.SolrSecurity != nil &&
solrCloud.Spec.SolrSecurity.ProbesRequireAuth {
+ if solrCloud.Spec.SolrSecurity != nil &&
solrCloud.Spec.SolrSecurity.ProbesRequireAuth &&
solrCloud.Spec.SolrSecurity.AuthenticationType == solr.Basic {
vol, volMount :=
secureProbeVolumeAndMount(solrCloud.BasicAuthSecretName())
if vol != nil {
stateful.Spec.Template.Spec.Volumes =
append(stateful.Spec.Template.Spec.Volumes, *vol)
@@ -177,26 +212,31 @@ func cmdToPutSecurityJsonInZk() string {
return fmt.Sprintf(cmd, scriptsDir, scriptsDir)
}
-func (security *SecurityConfig) AuthHeader() map[string]string {
- if security.BasicAuthSecret != nil {
- return map[string]string{"Authorization":
BasicAuthHeader(security.BasicAuthSecret)}
+// Add auth data to the supplied Context using secrets already resolved
(stored in the SecurityConfig)
+func (security *SecurityConfig) AddAuthToContext(ctx context.Context)
(context.Context, error) {
+ if security.SolrSecurity.AuthenticationType == solr.Basic {
+ return contextWithBasicAuthHeader(ctx,
security.CredentialsSecret), nil
}
- return nil
+ return ctx, nil
}
-func BasicAuthEnvVars(secretName string) []corev1.EnvVar {
- lor := corev1.LocalObjectReference{Name: secretName}
- usernameRef := &corev1.SecretKeySelector{LocalObjectReference: lor,
Key: corev1.BasicAuthUsernameKey}
- passwordRef := &corev1.SecretKeySelector{LocalObjectReference: lor,
Key: corev1.BasicAuthPasswordKey}
- return []corev1.EnvVar{
- {Name: "BASIC_AUTH_USER", ValueFrom:
&corev1.EnvVarSource{SecretKeyRef: usernameRef}},
- {Name: "BASIC_AUTH_PASS", ValueFrom:
&corev1.EnvVarSource{SecretKeyRef: passwordRef}},
+// Similar to security.AddAuthToContext but we need to lookup the secret
containing the authn credentials first
+func AddAuthToContext(ctx context.Context, client *client.Client, solrSecurity
*solr.SolrSecurityOptions, ns string) (context.Context, error) {
+ reader := *client
+ if solrSecurity.AuthenticationType == solr.Basic {
+ basicAuthSecret := &corev1.Secret{}
+ if err := reader.Get(ctx, types.NamespacedName{Name:
solrSecurity.BasicAuthSecret, Namespace: ns}, basicAuthSecret); err != nil {
+ return nil, err
+ }
+ return contextWithBasicAuthHeader(ctx, basicAuthSecret), nil
}
+ return ctx, nil
}
-func BasicAuthHeader(basicAuthSecret *corev1.Secret) string {
+func contextWithBasicAuthHeader(ctx context.Context, basicAuthSecret
*corev1.Secret) context.Context {
creds := fmt.Sprintf("%s:%s",
basicAuthSecret.Data[corev1.BasicAuthUsernameKey],
basicAuthSecret.Data[corev1.BasicAuthPasswordKey])
- return "Basic " + b64.StdEncoding.EncodeToString([]byte(creds))
+ headerValue := "Basic " + b64.StdEncoding.EncodeToString([]byte(creds))
+ return context.WithValue(ctx, solr_api.HTTP_HEADERS_CONTEXT_KEY,
map[string]string{"Authorization": headerValue})
}
func ValidateBasicAuthSecret(basicAuthSecret *corev1.Secret) error {
@@ -204,17 +244,19 @@ func ValidateBasicAuthSecret(basicAuthSecret
*corev1.Secret) error {
return fmt.Errorf("invalid secret type %v; user-provided secret
%s must be of type: %v",
basicAuthSecret.Type, basicAuthSecret.Name,
corev1.SecretTypeBasicAuth)
}
+ return validateCredentialsSecretData(basicAuthSecret, solr.Basic,
corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey)
+}
- if _, ok := basicAuthSecret.Data[corev1.BasicAuthUsernameKey]; !ok {
- return fmt.Errorf("%s key not found in user-provided basic-auth
secret %s",
- corev1.BasicAuthUsernameKey, basicAuthSecret.Name)
+func validateCredentialsSecretData(credsSecret *corev1.Secret, authType
solr.AuthenticationType, userKey string, passKey string) error {
+ if _, ok := credsSecret.Data[userKey]; !ok {
+ return fmt.Errorf("required key '%s' not found in user-provided
%s auth secret %s",
+ userKey, authType, credsSecret.Name)
}
- if _, ok := basicAuthSecret.Data[corev1.BasicAuthPasswordKey]; !ok {
- return fmt.Errorf("%s key not found in user-provided basic-auth
secret %s",
- corev1.BasicAuthPasswordKey, basicAuthSecret.Name)
+ if _, ok := credsSecret.Data[passKey]; !ok {
+ return fmt.Errorf("required key '%s' not found in user-provided
%s auth secret %s",
+ passKey, authType, credsSecret.Name)
}
-
return nil
}
@@ -393,6 +435,16 @@ func secureProbeVolumeAndMount(secretName string)
(*corev1.Volume, *corev1.Volum
return vol, volMount
}
+func BasicAuthEnvVars(secretName string) []corev1.EnvVar {
+ lor := corev1.LocalObjectReference{Name: secretName}
+ usernameRef := &corev1.SecretKeySelector{LocalObjectReference: lor,
Key: corev1.BasicAuthUsernameKey}
+ passwordRef := &corev1.SecretKeySelector{LocalObjectReference: lor,
Key: corev1.BasicAuthPasswordKey}
+ return []corev1.EnvVar{
+ {Name: "BASIC_AUTH_USER", ValueFrom:
&corev1.EnvVarSource{SecretKeyRef: usernameRef}},
+ {Name: "BASIC_AUTH_PASS", ValueFrom:
&corev1.EnvVarSource{SecretKeyRef: passwordRef}},
+ }
+}
+
// When running with TLS and clientAuth=Need or if the probe endpoints require
auth, we need to use a command instead of HTTP Get
// This function builds the custom probe command and returns any associated
volume / mounts needed for the auth secrets
func useSecureProbe(solrCloud *solr.SolrCloud, probe *corev1.Probe, mountPath
string) {
@@ -400,7 +452,7 @@ func useSecureProbe(solrCloud *solr.SolrCloud, probe
*corev1.Probe, mountPath st
//
https://kubernetes.io/docs/concepts/configuration/secret/#environment-variables-are-not-updated-after-a-secret-update
basicAuthOption := ""
enableBasicAuth := ""
- if solrCloud.Spec.SolrSecurity != nil &&
solrCloud.Spec.SolrSecurity.ProbesRequireAuth {
+ if solrCloud.Spec.SolrSecurity != nil &&
solrCloud.Spec.SolrSecurity.ProbesRequireAuth &&
solrCloud.Spec.SolrSecurity.AuthenticationType == solr.Basic {
usernameFile := fmt.Sprintf("%s/%s", mountPath,
corev1.BasicAuthUsernameKey)
passwordFile := fmt.Sprintf("%s/%s", mountPath,
corev1.BasicAuthPasswordKey)
basicAuthOption = fmt.Sprintf("-Dbasicauth=$(cat %s):$(cat
%s)", usernameFile, passwordFile)
@@ -431,3 +483,22 @@ func useSecureProbe(solrCloud *solr.SolrCloud, probe
*corev1.Probe, mountPath st
probe.TimeoutSeconds = 5
}
}
+
+// Called during reconcile to load the security.json from a user-supplied
secret
+func loadSecurityJsonFromSecret(ctx context.Context, client *client.Client,
securityJsonSecret *corev1.SecretKeySelector, ns string) (string, error) {
+ sec := &corev1.Secret{}
+ nn := types.NamespacedName{Name: securityJsonSecret.Name, Namespace: ns}
+ reader := *client
+ err := reader.Get(ctx, nn, sec)
+ if err != nil {
+ return "", err
+ }
+
+ securityJson, hasSecurityJson := sec.Data[securityJsonSecret.Key]
+ if !hasSecurityJson {
+ return "", fmt.Errorf("required key '%s' not found in the
user-supplied secret %s",
+ securityJsonSecret.Key, sec.Name)
+ }
+
+ return string(securityJson), nil
+}
diff --git a/controllers/util/solr_update_util.go
b/controllers/util/solr_update_util.go
index 72f373d..5ce6038 100644
--- a/controllers/util/solr_update_util.go
+++ b/controllers/util/solr_update_util.go
@@ -18,6 +18,7 @@
package util
import (
+ "context"
"fmt"
solr "github.com/apache/solr-operator/api/v1beta1"
"github.com/apache/solr-operator/controllers/util/solr_api"
@@ -93,7 +94,7 @@ func scheduleNextRestartWithTime(restartSchedule string,
podTemplateAnnotations
// TODO:
// - Think about caching this for ~250 ms? Not a huge need to send these
requests milliseconds apart.
// - Might be too much complexity for very little gain.
-func DeterminePodsSafeToUpdate(cloud *solr.SolrCloud, outOfDatePods
[]corev1.Pod, readyPods int, availableUpdatedPodCount int,
outOfDatePodsNotStartedCount int, logger logr.Logger, httpHeaders
map[string]string) (podsToUpdate []corev1.Pod, retryLater bool) {
+func DeterminePodsSafeToUpdate(ctx context.Context, cloud *solr.SolrCloud,
outOfDatePods []corev1.Pod, readyPods int, availableUpdatedPodCount int,
outOfDatePodsNotStartedCount int, logger logr.Logger) (podsToUpdate
[]corev1.Pod, retryLater bool) {
// Before fetching the cluster state, be sure that there is room to
update at least 1 pod
maxPodsUnavailable, unavailableUpdatedPodCount, maxPodsToUpdate :=
calculateMaxPodsToUpdate(cloud, len(outOfDatePods),
outOfDatePodsNotStartedCount, availableUpdatedPodCount)
if maxPodsToUpdate <= 0 {
@@ -106,13 +107,13 @@ func DeterminePodsSafeToUpdate(cloud *solr.SolrCloud,
outOfDatePods []corev1.Pod
if readyPods > 0 {
queryParams := url.Values{}
queryParams.Add("action", "CLUSTERSTATUS")
- err := solr_api.CallCollectionsApi(cloud, queryParams,
httpHeaders, clusterResp)
+ err := solr_api.CallCollectionsApi(ctx, cloud,
queryParams, clusterResp)
if err == nil {
if hasError, apiErr :=
solr_api.CheckForCollectionsApiError("CLUSTERSTATUS",
clusterResp.ResponseHeader); hasError {
err = apiErr
} else {
queryParams.Set("action",
"OVERSEERSTATUS")
- err =
solr_api.CallCollectionsApi(cloud, queryParams, httpHeaders, overseerResp)
+ err = solr_api.CallCollectionsApi(ctx,
cloud, queryParams, overseerResp)
if hasError, apiErr :=
solr_api.CheckForCollectionsApiError("OVERSEERSTATUS",
clusterResp.ResponseHeader); hasError {
err = apiErr
}
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index 9f41e22..c3b4a44 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -1027,11 +1027,7 @@ func generateZKInteractionInitContainer(solrCloud
*solr.SolrCloud, solrCloudStat
}
if security != nil && security.SecurityJson != "" {
- envVars = append(envVars, corev1.EnvVar{Name: "SECURITY_JSON",
ValueFrom: &corev1.EnvVarSource{
- SecretKeyRef: &corev1.SecretKeySelector{
- LocalObjectReference:
corev1.LocalObjectReference{Name: solrCloud.SecurityBootstrapSecretName()},
- Key: SecurityJsonFile}}})
-
+ envVars = append(envVars, corev1.EnvVar{Name: "SECURITY_JSON",
ValueFrom: security.SecurityJsonSrc})
cmd += cmdToPutSecurityJsonInZk()
}
diff --git a/docs/solr-cloud/solr-cloud-crd.md
b/docs/solr-cloud/solr-cloud-crd.md
index fb3bea9..67c2ba1 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -798,13 +798,12 @@ basic authentication with TLS to ensure credentials are
never passed in clear te
For background on Solr security, please refer to the [Reference
Guide](https://solr.apache.org/guide) for your version of Solr.
-Basic authentication is the only authentication scheme supported by the Solr
operator at this time. In general, you have
-two basic options for configuring basic authentication with the Solr operator:
+The Solr operator only supports the `Basic` authentication scheme. In general,
you have two primary options for configuring authentication with the Solr
operator:
1. Let the Solr operator bootstrap the `security.json` to configure *basic
authentication* for Solr.
2. Supply your own `security.json` to Solr, which must define a user account
that the operator can use to make API requests to secured Solr pods.
-If you choose option 2, then you need to provide the credentials to the Solr
operator using a Kubernetes [Basic Authentication
Secret](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret).
-With option 1, the operator creates the Basic Authentication Secret for you.
+If you choose option 2, then you need to provide the credentials the Solr
operator should use to make requests to Solr via a Kubernetes secret.
+With option 1, the operator creates a Basic Authentication Secret for you,
which contains the username and password for the `k8s-oper` user.
### Option 1: Bootstrap Security
@@ -1009,6 +1008,12 @@ The exporter also hits the `/admin/ping` endpoint for
every collection, which re
"collection": "*",
"path": "/admin/ping"
},
+ {
+ "name": "k8s-zk",
+ "role":"k8s",
+ "collection": null,
+ "path":"/admin/zookeeper/status"
+ },
```
The `"collection":"*"` setting indicates this path applies to all collections,
which maps to endpoint `/collections/<COLL>/admin/ping` at runtime.
@@ -1016,9 +1021,33 @@ The initial authorization config grants the `read`
permission to the `users` rol
For instance, the `solr` user is mapped to the `users` role, so the `solr`
user can send query requests only.
In general, please verify the initial authorization rules for each role before
sharing user credentials.
-### Option 2: User-provided Basic Auth Secret
+### Option 2: User-provided `security.json` and credentials secret
+
+If users want full control over their cluster's security config, then they can
provide the Solr `security.json` via a Secret and the credentials the operator
should use
+to make requests to Solr in a Secret.
+
+#### Custom `security.json` Secret
+_Since v0.5.0_
+
+For full control over the Solr security configuration, supply a
`security.json` in a Secret. The following example illustrates how to point the
operator to a Secret containing a custom `security.json`:
-Alternatively, if users want full control over their cluster's security
config, then they can provide a `kubernetes.io/basic-auth` secret containing
the credentials for the user they want the operator to make API requests as:
+```yaml
+spec:
+ ...
+ solrSecurity:
+ authenticationType: Basic
+ bootstrapSecurityJson:
+ name: my-custom-security-json
+ key: security.json
+```
+For `Basic` authentication, if you don't supply a `security.json` Secret, then
the operator assumes you are bootstrapping the security configuration via some
other means.
+
+Refer to the example `security.json` shown in the Authorization section above
to help you get started crafting your own custom configuration.
+
+#### Basic Authentication
+
+For `Basic` authentication, the supplied secret must be of type [Basic
Authentication
Secret](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret)
and define both a `username` and `password`.
+
```yaml
spec:
...
@@ -1026,7 +1055,7 @@ spec:
authenticationType: Basic
basicAuthSecret: user-provided-secret
```
-The supplied secret must be of type [Basic Authentication
Secret](https://kubernetes.io/docs/concepts/configuration/secret/#basic-authentication-secret)
and define both a `username` and `password`.
+
Here is an example of how to define a basic auth secret using YAML:
```yaml
apiVersion: v1
@@ -1041,25 +1070,23 @@ stringData:
With this config, the operator will make API requests to secured Solr pods as
the `k8s-oper` user.
_Note: be sure to use a stronger password for real deployments_
-If users supply their own basic auth secret, then the operator *does not*
bootstrap the `security.json`;
-the reasoning is that if you're supplying your own basic auth credentials then
you're also assuming the responsibility for configuring the desired access for
this user.
-
-Users need to ensure their `security.json` contains the user supplied in the
`basicAuthSecret` with read access to:
+Users need to ensure their `security.json` contains the user supplied in the
`basicAuthSecret` has read access to the following endpoints:
```
/admin/info/system
/admin/info/health
/admin/collections
/admin/metrics
/admin/ping (for collection="*")
+/admin/zookeeper/status
```
_Tip: see the authorization rules defined by the default `security.json` as a
guide for configuring access for the operator user_
-#### Changing the Password
+##### Changing the Password
If you change the password for the user configured in your `basicAuthSecret`
using the Solr security API, then you **must** update the secret with the new
password or the operator will be locked out.
Also, changing the password for this user in the K8s secret will not update
Solr! You're responsible for changing the password in both places.
-### Prometheus Exporter with Basic Auth
+##### Prometheus Exporter with Basic Auth
If you enable basic auth for your SolrCloud cluster, then you need to point
the Prometheus exporter at the basic auth secret;
refer to [Prometheus Exporter with Basic
Auth](../solr-prometheus-exporter/README.md#prometheus-exporter-with-basic-auth)
for more details.
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index c9f87cd..a2da167 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -141,6 +141,13 @@ annotations:
url: https://github.com/apache/solr-operator/pull/350
- name: Topology Spread Constraints Documentation
url:
https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/
+ - kind: added
+ description: Ability to bootstrap security configuration from a
security.json in a user-supplied secret
+ links:
+ - name: Github Issue
+ url: https://github.com/apache/solr-operator/issues/355
+ - name: Github PR
+ url: https://github.com/apache/solr-operator/pull/356
artifacthub.io/images: |
- name: solr-operator
image: apache/solr-operator:v0.5.0-prerelease
diff --git a/helm/solr-operator/crds/crds.yaml
b/helm/solr-operator/crds/crds.yaml
index 43a9551..68a1e75 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -7029,6 +7029,21 @@ spec:
basicAuthSecret:
description: "Secret (kubernetes.io/basic-auth) containing
credentials the operator should use for API requests to secure Solr pods. If
you provide this secret, then the operator assumes you've also configured your
own security.json file and uploaded it to Solr. If you change the password for
this user using the Solr security API, then you *must* update the secret with
the new password or the operator will be locked out of Solr and API requests
will fail, ultimately [...]
type: string
+ bootstrapSecurityJson:
+ description: Configure a user-provided security.json from
a secret to allow for advanced security config. If not specified, the operator
bootstraps a security.json with basic auth enabled. This is a bootstrapping
config only; once Solr is initialized, the security config should be managed by
the security API.
+ properties:
+ key:
+ description: The key of the secret to select from.
Must be a valid secret key.
+ type: string
+ name:
+ description: 'Name of the referent. More info:
https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?'
+ type: string
+ optional:
+ description: Specify whether the Secret or its key
must be defined
+ type: boolean
+ required:
+ - key
+ type: object
probesRequireAuth:
description: Flag to indicate if the configured HTTP
endpoint(s) used for the probes require authentication; defaults to false. If
you set to true, then probes will use a local command on the main container to
hit the secured endpoints with credentials sourced from an env var instead of
HTTP directly.
type: boolean
diff --git a/helm/solr/README.md b/helm/solr/README.md
index 4647587..75a22b0 100644
--- a/helm/solr/README.md
+++ b/helm/solr/README.md
@@ -95,6 +95,8 @@ The command removes the SolrCloud resource, and then
Kubernetes will garbage col
| solrOptions.security.authenticationType | string | `""` | Type of
authentication to use for Solr |
| solrOptions.security.basicAuthSecret | string | `""` | Name of Secret in the
same namespace that stores the basicAuth information for the Solr user |
| solrOptions.security.probesRequireAuth | boolean | | Whether the probes for
the SolrCloud pod require auth |
+| solrOptions.security.bootstrapSecurityJson.name | string | | Name of a
Secret in the same namespace that stores a user-provided `security.json` to
bootstrap the Solr security config |
+| solrOptions.security.bootstrapSecurityJson.key | string | | Key holding the
user-provided `security.json` in the bootstrap security Secret |
| updateStrategy.method | string | `"Managed"` | The method for conducting
updates of Solr pods. Either `Managed`, `StatefulSet` or `Manual`. See the
[docs](https://apache.github.io/solr-operator/docs/solr-cloud/solr-cloud-crd.html#update-strategy)
for more information |
| updateStrategy.managedUpdate.maxPodsUnavailable | int-or-string | `"25%"` |
The number of Solr pods in a Solr Cloud that are allowed to be unavailable
during the rolling restart. Either a static number, or a percentage
representing the percentage of total pods requested for the statefulSet. |
| updateStrategy.managedUpdate.maxShardReplicasUnavailable | int-or-string |
`1` | The number of replicas for each shard allowed to be unavailable during
the restart. Either a static number, or a percentage representing the
percentage of the number of replicas for a shard. |
diff --git a/helm/solr/values.yaml b/helm/solr/values.yaml
index f422820..bd6c2cc 100644
--- a/helm/solr/values.yaml
+++ b/helm/solr/values.yaml
@@ -64,6 +64,9 @@ solrOptions:
# authenticationType: Basic
# basicAuthSecret: secret-name
# probesRequireAuth: false
+ # bootstrapSecurityJson:
+ # name: my-custom-security-json-secret
+ # key: security.json
# Specify how the SolrCloud should be addressable