blob: c9facb5f830b0a266d24a9f73fb4e7709ffe62cc [file] [log] [blame]
/*
* 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 controllers
import (
"context"
"fmt"
solrv1beta1 "github.com/apache/solr-operator/api/v1beta1"
"github.com/apache/solr-operator/controllers/util"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
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() {
var (
ctx context.Context
solrCloud *solrv1beta1.SolrCloud
)
BeforeEach(func() {
ctx = context.Background()
replicas := int32(1)
solrCloud = &solrv1beta1.SolrCloud{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "default",
},
Spec: solrv1beta1.SolrCloudSpec{
Replicas: &replicas,
ZookeeperRef: &solrv1beta1.ZookeeperRef{
ConnectionInfo: &solrv1beta1.ZookeeperConnectionInfo{
InternalConnectionString: "host:7271",
},
},
},
}
})
JustBeforeEach(func() {
By("creating the SolrCloud")
Expect(k8sClient.Create(ctx, solrCloud)).To(Succeed())
By("defaulting the missing SolrCloud values")
expectSolrCloudWithChecks(ctx, solrCloud, func(g Gomega, found *solrv1beta1.SolrCloud) {
g.Expect(found.WithDefaults(logger)).To(BeFalse(), "The SolrCloud spec should not need to be defaulted eventually")
})
})
AfterEach(func() {
cleanupTest(ctx, solrCloud)
})
FContext("Boostrap Security JSON", func() {
BeforeEach(func() {
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 Custom Probe Paths", func() {
BeforeEach(func() {
customHandler := corev1.ProbeHandler{
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{ProbeHandler: customHandler},
ReadinessProbe: &corev1.Probe{ProbeHandler: 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{
AuthenticationType: solrv1beta1.Basic,
ProbesRequireAuth: true,
}
solrCloud.Spec.ZookeeperRef.ConnectionInfo.AllACL = &solrv1beta1.ZookeeperACL{
SecretRef: "secret-name",
UsernameKey: "user",
PasswordKey: "pass",
}
solrCloud.Spec.ZookeeperRef.ConnectionInfo.ReadOnlyACL = &solrv1beta1.ZookeeperACL{
SecretRef: "read-secret-name",
UsernameKey: "read-only-user",
PasswordKey: "read-only-pass",
}
})
FIt("has the correct resources", func() {
expectStatefulSetBasicAuthConfig(ctx, solrCloud, true)
})
})
FContext("Boostrap Security JSON - Delete Secret", func() {
BeforeEach(func() {
solrCloud.Spec.SolrSecurity = &solrv1beta1.SolrSecurityOptions{
AuthenticationType: solrv1beta1.Basic,
}
})
FIt("has the correct resources", func() {
By("Testing that the statefulSet exists and has the correct information")
expectStatefulSetBasicAuthConfig(ctx, solrCloud, true)
By("Deleting bootstrapped secret")
expectSecretWithChecks(ctx, solrCloud, solrCloud.SecurityBootstrapSecretName(), func(g Gomega, found *corev1.Secret) {
g.Expect(k8sClient.Delete(ctx, found)).To(Succeed(), "Cannot delete bootstrapped secret")
})
By("Making sure the statefulSet is updated after deleting the bootstrap secret")
expectStatefulSetBasicAuthConfig(ctx, solrCloud, false /* bootstrap secret not found */)
})
})
FContext("User Provided Credentials", func() {
BeforeEach(func() {
basicAuthSecretName := "my-basic-auth-secret"
solrCloud.Spec.SolrSecurity = &solrv1beta1.SolrSecurityOptions{
AuthenticationType: solrv1beta1.Basic,
BasicAuthSecret: basicAuthSecretName,
}
})
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("Make sure the StatefulSet is created and configured correctly")
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{
"admin",
"solr",
"security.json",
}
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, expProbePath)
})
expectSecretWithChecks(ctx, sc, sc.BasicAuthSecretName(), func(innerG Gomega, found *corev1.Secret) {
innerG.Expect(found.Data).To(HaveKey(corev1.BasicAuthUsernameKey), "The Basic auth secret does not have a username entry for key %s", corev1.BasicAuthUsernameKey)
innerG.Expect(string(found.Data[corev1.BasicAuthUsernameKey])).To(Equal(solrv1beta1.DefaultBasicAuthUsername), "password should be set for k8s-oper in the basic auth secret")
innerG.Expect(found.Data).To(HaveKey(corev1.BasicAuthPasswordKey), "The Basic auth secret does not have a password entry for key %s", corev1.BasicAuthPasswordKey)
innerG.Expect(found.Data[corev1.BasicAuthPasswordKey]).To(Not(BeEmpty()), "password should be set for k8s-oper in the basic auth secret and not be empty")
})
// verify a security.json gets bootstrapped if not using a user-provided secret
if sc.Spec.SolrSecurity.BasicAuthSecret == "" && expectBootstrapSecret {
bootstrapSecret := expectSecretWithChecks(ctx, sc, sc.SecurityBootstrapSecretName(), func(g Gomega, found *corev1.Secret) {
for _, key := range boostrapedSecretKeys {
g.Expect(found.Data).To(HaveKey(key), "The Boostrapped Security secret does not key %s", key)
g.Expect(found.Data[key]).To(Not(BeEmpty()), "The Boostrapped Security secret key should not be empty: %s", key)
}
})
if sc.Spec.CustomSolrKubeOptions.PodOptions != nil {
probePaths := util.GetCustomProbePaths(sc)
if len(probePaths) > 0 {
securityJson := string(bootstrapSecret.Data["security.json"])
Expect(securityJson).To(ContainSubstring(util.DefaultProbePath), "bootstrapped security.json should have an authz rule for probe path: %s", util.DefaultProbePath)
for _, p := range probePaths {
p = p[len("/solr"):] // drop the /solr part on the path
Expect(securityJson).To(ContainSubstring(p), "bootstrapped security.json should have an authz rule for probe path: %s", p)
}
}
}
}
return stateful
}
// Ensures config is setup for basic-auth enabled Solr pods
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]
g.Expect(mainContainer).To(Not(BeNil()), "Didn't find the main solrcloud-node container in the sts!")
g.Expect(mainContainer.Env).To(Not(BeEmpty()), "Didn't find the main solrcloud-node container in the sts!")
// probes with auth
if solrCloud.Spec.SolrSecurity.ProbesRequireAuth {
g.Expect(podTemplate.Spec.Volumes).To(Not(BeEmpty()), "Solr Pod requires volumes when using BasicAuth")
secretName := solrCloud.BasicAuthSecretName()
var basicAuthSecretVol *corev1.Volume = nil
for _, vol := range podTemplate.Spec.Volumes {
if vol.Name == secretName {
basicAuthSecretVol = &vol
break
}
}
g.Expect(basicAuthSecretVol).To(Not(BeNil()), "No basic auth secret volume found in Pod spec")
g.Expect(basicAuthSecretVol.VolumeSource.Secret).To(Not(BeNil()), "Didn't find the basic auth secret volume in sts config!")
g.Expect(basicAuthSecretVol.VolumeSource.Secret.SecretName).To(Equal(solrCloud.BasicAuthSecretName()), "Wrong secret name used for basic auth volume")
var basicAuthSecretVolMount *corev1.VolumeMount = nil
for _, m := range podTemplate.Spec.Containers[0].VolumeMounts {
if m.Name == secretName {
basicAuthSecretVolMount = &m
break
}
}
g.Expect(basicAuthSecretVolMount).To(Not(BeNil()), "No Basic Auth volume mount used in Solr container")
g.Expect(basicAuthSecretVolMount.MountPath).To(Equal("/etc/secrets/"+secretName), "Wrong path used to mount Basic Auth volume")
expProbeCmd := fmt.Sprintf("JAVA_TOOL_OPTIONS=\"-Dbasicauth=$(cat /etc/secrets/%s-solrcloud-basic-auth/username):$(cat /etc/secrets/%s-solrcloud-basic-auth/password)\" java "+
"-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%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")
g.Expect(mainContainer.LivenessProbe.Exec.Command[2]).To(Equal(expProbeCmd), "liveness probe should invoke java with auth opts")
g.Expect(mainContainer.LivenessProbe.TimeoutSeconds).To(BeEquivalentTo(5), "liveness probe default timeout should be increased when using basicAuth")
g.Expect(mainContainer.ReadinessProbe).To(Not(BeNil()), "main container should have a readiness probe defined")
g.Expect(mainContainer.ReadinessProbe.Exec).To(Not(BeNil()), "readiness probe should have an exec when auth is enabled")
g.Expect(mainContainer.ReadinessProbe.Exec.Command).To(Not(BeEmpty()), "readiness probe command cannot be empty")
g.Expect(mainContainer.ReadinessProbe.Exec.Command[2]).To(Equal(expProbeCmd), "readiness probe should invoke java with auth opts")
g.Expect(mainContainer.ReadinessProbe.TimeoutSeconds).To(BeEquivalentTo(5), "readiness probe default timeout should be increased when using basicAuth")
}
// if no user-provided auth secret, then check that security.json gets bootstrapped correctly
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
for _, cnt := range podTemplate.Spec.InitContainers {
if cnt.Name == "setup-zk" {
expInitContainer = &cnt
break
}
}
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 {
g.Expect(expInitContainer.Env).To(HaveLen(10), "Wrong number of env vars using ACLs and Basic Auth")
g.Expect(expInitContainer.Env[len(expInitContainer.Env)-2].Name).To(Equal("SOLR_OPTS"), "Env var SOLR_OPTS is misplaced the Solr Pod env vars")
g.Expect(expInitContainer.Env[len(expInitContainer.Env)-1].Name).To(Equal("SECURITY_JSON"), "Env var SECURITY_JSON is misplaced the Solr Pod env vars")
testACLEnvVarsWithGomega(g, expInitContainer.Env[3:len(expInitContainer.Env)-2], true)
} // else this ref not using ACLs
expectPutSecurityJsonInZkCmd(g, expInitContainer)
} else {
g.Expect(expInitContainer).To(Or(
BeNil(),
WithTransform(
func(container corev1.Container) string {
if len(container.Command) < 3 {
return ""
}
return container.Command[2]
},
Not(ContainSubstring("SECURITY_JSON")))), "setup-zk initContainer not reconciled after bootstrap secret deleted")
}
}
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
}