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 1548258 Allow users to supply ACL credentials for provided ZK ensembles to configure Solr to set ACLs on znodes (#253) 1548258 is described below commit 1548258ec07bfde23e97f0b428101be752f8da70 Author: Timothy Potter <thelabd...@gmail.com> AuthorDate: Fri Apr 9 11:06:15 2021 -0600 Allow users to supply ACL credentials for provided ZK ensembles to configure Solr to set ACLs on znodes (#253) --- api/v1beta1/solrcloud_types.go | 25 +++++ api/v1beta1/zz_generated.deepcopy.go | 17 ++++ config/crd/bases/solr.apache.org_solrclouds.yaml | 114 +++++++++++++++++++++ controllers/controller_utils_test.go | 67 +++++++++++++ controllers/solrcloud_controller_tls_test.go | 32 ++++++ controllers/solrcloud_controller_zk_test.go | 120 +++++++++++++---------- controllers/util/prometheus_exporter_util.go | 2 +- controllers/util/solr_util.go | 3 +- controllers/util/zk_util.go | 35 ++++--- docs/solr-cloud/solr-cloud-crd.md | 19 +++- helm/solr-operator/crds/crds.yaml | 114 +++++++++++++++++++++ 11 files changed, 479 insertions(+), 69 deletions(-) diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go index 49ecd00..d2eeb13 100644 --- a/api/v1beta1/solrcloud_types.go +++ b/api/v1beta1/solrcloud_types.go @@ -556,6 +556,17 @@ func (ref *ZookeeperRef) withDefaults() (changed bool) { return changed } +func (ref *ZookeeperRef) GetACLs() (allACL *ZookeeperACL, readOnlyACL *ZookeeperACL) { + if ref.ConnectionInfo != nil { + allACL = ref.ConnectionInfo.AllACL + readOnlyACL = ref.ConnectionInfo.ReadOnlyACL + } else if ref.ProvidedZookeeper != nil { + allACL = ref.ProvidedZookeeper.AllACL + readOnlyACL = ref.ProvidedZookeeper.ReadOnlyACL + } + return +} + // ZookeeperSpec defines the internal zookeeper ensemble to run with the given spec type ZookeeperSpec struct { @@ -580,6 +591,16 @@ type ZookeeperSpec struct { // The ChRoot to connect solr at // +optional ChRoot string `json:"chroot,omitempty"` + + // ZooKeeper ACL to use when connecting with ZK. + // This ACL should have ALL permission in the given chRoot. + // +optional + AllACL *ZookeeperACL `json:"acl,omitempty"` + + // ZooKeeper ACL to use when connecting with ZK for reading operations. + // This ACL should have READ permission in the given chRoot. + // +optional + ReadOnlyACL *ZookeeperACL `json:"readOnlyAcl,omitempty"` } func (z *ZookeeperSpec) withDefaults() (changed bool) { @@ -640,6 +661,10 @@ type ZookeeperPodPolicy struct { // +optional Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + // List of environment variables to set in the main ZK container. + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` + // Resources is the resource requirements for the container. // This field cannot be updated once the cluster is created. // +optional diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 6810efe..b93ff93 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -1270,6 +1270,13 @@ func (in *ZookeeperPodPolicy) DeepCopyInto(out *ZookeeperPodPolicy) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } in.Resources.DeepCopyInto(&out.Resources) } @@ -1327,6 +1334,16 @@ func (in *ZookeeperSpec) DeepCopyInto(out *ZookeeperSpec) { (*in).DeepCopyInto(*out) } in.ZookeeperPod.DeepCopyInto(&out.ZookeeperPod) + if in.AllACL != nil { + in, out := &in.AllACL, &out.AllACL + *out = new(ZookeeperACL) + **out = **in + } + if in.ReadOnlyACL != nil { + in, out := &in.ReadOnlyACL, &out.ReadOnlyACL + *out = new(ZookeeperACL) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZookeeperSpec. diff --git a/config/crd/bases/solr.apache.org_solrclouds.yaml b/config/crd/bases/solr.apache.org_solrclouds.yaml index d27e6c1..f4fa881 100644 --- a/config/crd/bases/solr.apache.org_solrclouds.yaml +++ b/config/crd/bases/solr.apache.org_solrclouds.yaml @@ -4666,6 +4666,23 @@ spec: provided: description: 'Create a new Zookeeper Ensemble with the following spec Note: This option will not allow the SolrCloud to run across kube-clusters. Note: Requires - The zookeeperOperator flag to be provided to the Solr Operator - A zookeeper operator to be running' properties: + acl: + description: ZooKeeper ACL to use when connecting with ZK. This ACL should have ALL permission in the given chRoot. + properties: + passwordKey: + description: The name of the key in the given secret that contains the ACL password + type: string + secret: + description: The name of the Kubernetes Secret that stores the username and password for the ACL. This secret must be in the same namespace as the solrCloud or prometheusExporter is running in. + type: string + usernameKey: + description: The name of the key in the given secret that contains the ACL username + type: string + required: + - passwordKey + - secret + - usernameKey + type: object chroot: description: The ChRoot to connect solr at type: string @@ -4778,6 +4795,23 @@ spec: type: string type: object type: object + readOnlyAcl: + description: ZooKeeper ACL to use when connecting with ZK for reading operations. This ACL should have READ permission in the given chRoot. + properties: + passwordKey: + description: The name of the key in the given secret that contains the ACL password + type: string + secret: + description: The name of the Kubernetes Secret that stores the username and password for the ACL. This secret must be in the same namespace as the solrCloud or prometheusExporter is running in. + type: string + usernameKey: + description: The name of the key in the given secret that contains the ACL username + type: string + required: + - passwordKey + - secret + - usernameKey + type: object replicas: description: Number of members to create up for the ZK ensemble Defaults to 3 format: int32 @@ -5126,6 +5160,86 @@ spec: type: array type: object type: object + env: + description: List of environment variables to set in the main ZK container. + items: + description: EnvVar represents an environment variable present in a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) are expanded using the previous defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. The $(VAR_NAME) syntax can be escaped with a double $$, ie: $$(VAR_NAME). Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment variable's value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + 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 ConfigMap or its key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels[''<KEY>'']`, `metadata.annotations[''<KEY>'']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + 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 + type: object + required: + - name + type: object + type: array nodeSelector: additionalProperties: type: string diff --git a/controllers/controller_utils_test.go b/controllers/controller_utils_test.go index 652e4b4..9430e7e 100644 --- a/controllers/controller_utils_test.go +++ b/controllers/controller_utils_test.go @@ -363,6 +363,73 @@ func testMapContainsOther(t *testing.T, mapName string, base map[string]string, } } +func testACLEnvVars(t *testing.T, actualEnvVars []corev1.EnvVar) { + /* + This test verifies ACL related env vars are set correctly and in the correct order, but expects a very specific config to be used in your test SolrCloud config: + + AllACL: &solr.ZookeeperACL{ + SecretRef: "secret-name", + UsernameKey: "user", + PasswordKey: "pass", + }, + ReadOnlyACL: &solr.ZookeeperACL{ + SecretRef: "read-secret-name", + UsernameKey: "read-only-user", + PasswordKey: "read-only-pass", + }, + + */ + f := false + zkAclEnvVars := []corev1.EnvVar{ + { + Name: "ZK_ALL_ACL_USERNAME", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "secret-name"}, + Key: "user", + Optional: &f, + }, + }, + }, + { + Name: "ZK_ALL_ACL_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "secret-name"}, + Key: "pass", + Optional: &f, + }, + }, + }, + { + Name: "ZK_READ_ACL_USERNAME", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "read-secret-name"}, + Key: "read-only-user", + Optional: &f, + }, + }, + }, + { + Name: "ZK_READ_ACL_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "read-secret-name"}, + Key: "read-only-pass", + Optional: &f, + }, + }, + }, + { + Name: "SOLR_ZK_CREDS_AND_ACLS", + Value: "-DzkACLProvider=org.apache.solr.common.cloud.VMParamsAllAndReadonlyDigestZkACLProvider -DzkCredentialsProvider=org.apache.solr.common.cloud.VMParamsSingleSetCredentialsDigestZkCredentialsProvider -DzkDigestUsername=$(ZK_ALL_ACL_USERNAME) -DzkDigestPassword=$(ZK_ALL_ACL_PASSWORD) -DzkDigestReadonlyUsername=$(ZK_READ_ACL_USERNAME) -DzkDigestReadonlyPassword=$(ZK_READ_ACL_PASSWORD)", + ValueFrom: nil, + }, + } + assert.Equal(t, zkAclEnvVars, actualEnvVars, "ZK ACL Env Vars are not correct") +} + func cleanupTest(g *gomega.GomegaWithT, namespace string) { deleteOpts := []client.DeleteAllOfOption{ client.InNamespace(namespace), diff --git a/controllers/solrcloud_controller_tls_test.go b/controllers/solrcloud_controller_tls_test.go index 2a52b42..db23661 100644 --- a/controllers/solrcloud_controller_tls_test.go +++ b/controllers/solrcloud_controller_tls_test.go @@ -55,6 +55,29 @@ func TestBasicAuthBootstrapSecurityJson(t *testing.T) { verifyReconcileWithSecurity(t, instance, false) } +func TestBasicAuthBootstrapSecurityJsonWithZkACLs(t *testing.T) { + UseZkCRD(true) + instance := buildTestSolrCloud() + instance.Spec.SolrSecurity = &solr.SolrSecurityOptions{AuthenticationType: solr.Basic, ProbesRequireAuth: true} + zkReplicas := int32(1) + instance.Spec.ZookeeperRef = &solr.ZookeeperRef{ + ProvidedZookeeper: &solr.ZookeeperSpec{ + Replicas: &zkReplicas, + AllACL: &solr.ZookeeperACL{ + SecretRef: "secret-name", + UsernameKey: "user", + PasswordKey: "pass", + }, + ReadOnlyACL: &solr.ZookeeperACL{ + SecretRef: "read-secret-name", + UsernameKey: "read-only-user", + PasswordKey: "read-only-pass", + }, + }, + } + verifyReconcileWithSecurity(t, instance, false) +} + func TestBasicAuthBootstrapSecurityJsonDeleteSecret(t *testing.T) { instance := buildTestSolrCloud() instance.Spec.SolrSecurity = &solr.SolrSecurityOptions{AuthenticationType: solr.Basic} @@ -473,6 +496,15 @@ func expectBasicAuthConfigOnPodTemplate(t *testing.T, instance *solr.SolrCloud, } if expectBootstrapSecret { + // if the zookeeperRef has ACLs set, verify the env vars were set correctly for this initContainer + allACL, _ := instance.Spec.ZookeeperRef.GetACLs() + if allACL != nil { + assert.Equal(t, 10, len(expInitContainer.Env)) + assert.Equal(t, "SOLR_OPTS", expInitContainer.Env[len(expInitContainer.Env)-2].Name) + assert.Equal(t, "SECURITY_JSON", expInitContainer.Env[len(expInitContainer.Env)-1].Name) + testACLEnvVars(t, expInitContainer.Env[3:len(expInitContainer.Env)-2]) + } // else this ref not using ACLs + assert.NotNil(t, expInitContainer, "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 " + diff --git a/controllers/solrcloud_controller_zk_test.go b/controllers/solrcloud_controller_zk_test.go index ae8e6fb..36699fc 100644 --- a/controllers/solrcloud_controller_zk_test.go +++ b/controllers/solrcloud_controller_zk_test.go @@ -18,6 +18,7 @@ package controllers import ( + "fmt" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "testing" @@ -166,11 +167,7 @@ func TestZKACLsCloudReconcile(t *testing.T) { } func TestBothZKACLsCloudReconcile(t *testing.T) { - UseZkCRD(false) - g := gomega.NewGomegaWithT(t) - replicas := int32(3) - instance := &solr.SolrCloud{ ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace}, Spec: solr.SolrCloudSpec{ @@ -198,6 +195,48 @@ func TestBothZKACLsCloudReconcile(t *testing.T) { SolrOpts: "-Dextra -Dopts", }, } + testZkACLsReconcile(t, false, instance, "host:7271/") +} + +func TestZKACLsForProvidedReconcile(t *testing.T) { + replicas := int32(3) + zkReplicas := int32(1) + instance := &solr.SolrCloud{ + ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace}, + Spec: solr.SolrCloudSpec{ + Replicas: &replicas, + ZookeeperRef: &solr.ZookeeperRef{ + ProvidedZookeeper: &solr.ZookeeperSpec{ + Replicas: &zkReplicas, + AllACL: &solr.ZookeeperACL{ + SecretRef: "secret-name", + UsernameKey: "user", + PasswordKey: "pass", + }, + ReadOnlyACL: &solr.ZookeeperACL{ + SecretRef: "read-secret-name", + UsernameKey: "read-only-user", + PasswordKey: "read-only-pass", + }, + }, + }, + CustomSolrKubeOptions: solr.CustomSolrKubeOptions{ + PodOptions: &solr.PodOptions{ + EnvVariables: extraVars, + }, + }, + SolrOpts: "-Dextra -Dopts", + }, + } + + expectedZkHost := fmt.Sprintf("%s-zookeeper-0.%s-zookeeper-headless.%s.svc.cluster.local:2181/", + cloudSsKey.Name, cloudSsKey.Name, cloudSsKey.Namespace) + testZkACLsReconcile(t, true, instance, expectedZkHost) +} + +func testZkACLsReconcile(t *testing.T, useZkCRD bool, instance *solr.SolrCloud, expectedZkHost string) { + UseZkCRD(useZkCRD) + g := gomega.NewGomegaWithT(t) // Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a // channel when it is finished. @@ -237,62 +276,14 @@ func TestBothZKACLsCloudReconcile(t *testing.T) { // Env Variable Tests expectedEnvVars := map[string]string{ - "ZK_HOST": "host:7271/", + "ZK_HOST": expectedZkHost, "SOLR_HOST": "$(POD_HOSTNAME)." + cloudHsKey.Name + "." + instance.Namespace, "SOLR_PORT": "8983", "SOLR_NODE_PORT": "8983", "SOLR_OPTS": "-DhostPort=$(SOLR_NODE_PORT) $(SOLR_ZK_CREDS_AND_ACLS) -Dextra -Dopts", } foundEnv := statefulSet.Spec.Template.Spec.Containers[0].Env - f := false - zkAclEnvVars := []corev1.EnvVar{ - { - Name: "ZK_ALL_ACL_USERNAME", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: "secret-name"}, - Key: "user", - Optional: &f, - }, - }, - }, - { - Name: "ZK_ALL_ACL_PASSWORD", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: "secret-name"}, - Key: "pass", - Optional: &f, - }, - }, - }, - { - Name: "ZK_READ_ACL_USERNAME", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: "read-secret-name"}, - Key: "read-only-user", - Optional: &f, - }, - }, - }, - { - Name: "ZK_READ_ACL_PASSWORD", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: "read-secret-name"}, - Key: "read-only-pass", - Optional: &f, - }, - }, - }, - { - Name: "SOLR_ZK_CREDS_AND_ACLS", - Value: "-DzkACLProvider=org.apache.solr.common.cloud.VMParamsAllAndReadonlyDigestZkACLProvider -DzkCredentialsProvider=org.apache.solr.common.cloud.VMParamsSingleSetCredentialsDigestZkCredentialsProvider -DzkDigestUsername=$(ZK_ALL_ACL_USERNAME) -DzkDigestPassword=$(ZK_ALL_ACL_PASSWORD) -DzkDigestReadonlyUsername=$(ZK_READ_ACL_USERNAME) -DzkDigestReadonlyPassword=$(ZK_READ_ACL_PASSWORD)", - ValueFrom: nil, - }, - } - assert.Equal(t, zkAclEnvVars, foundEnv[len(foundEnv)-8:len(foundEnv)-3], "ZK ACL Env Vars are not correct") + testACLEnvVars(t, foundEnv[len(foundEnv)-8:len(foundEnv)-3]) assert.Equal(t, extraVars, foundEnv[len(foundEnv)-3:len(foundEnv)-1], "Extra Env Vars are not the same as the ones provided in podOptions") // Note that this check changes the variable foundEnv, so the values are no longer valid afterwards. // TODO: Make this not invalidate foundEnv @@ -318,4 +309,25 @@ func TestBothZKACLsCloudReconcile(t *testing.T) { // Check the ingress expectNoIngress(g, cloudIKey) + + // now update the env vars on the zk spec + if instance.Spec.ZookeeperRef.ProvidedZookeeper != nil { + err = testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, instance) + g.Expect(err).NotTo(gomega.HaveOccurred()) + updateEnvVars := []corev1.EnvVar{ + { + Name: "VAR_1", + Value: "VAL_1", + }, + } + instance.Spec.ZookeeperRef.ProvidedZookeeper.ZookeeperPod = solr.ZookeeperPodPolicy{Env: updateEnvVars} + err = testClient.Update(context.TODO(), instance) + g.Expect(err).NotTo(gomega.HaveOccurred()) + g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest))) + g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest))) + lookup := &solr.SolrCloud{} + err = testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, lookup) + g.Expect(err).NotTo(gomega.HaveOccurred()) + assert.EqualValues(t, updateEnvVars, lookup.Spec.ZookeeperRef.ProvidedZookeeper.ZookeeperPod.Env, "Updated ZK Pod env not reconciled!") + } } diff --git a/controllers/util/prometheus_exporter_util.go b/controllers/util/prometheus_exporter_util.go index 25c10e9..f82c899 100644 --- a/controllers/util/prometheus_exporter_util.go +++ b/controllers/util/prometheus_exporter_util.go @@ -103,7 +103,7 @@ func GenerateSolrPrometheusExporterDeployment(solrPrometheusExporter *solr.SolrP exporterArgs = append(exporterArgs, "-z", solrConnectionInfo.CloudZkConnnectionInfo.ZkConnectionString()) // Add ACL information, if given, through Env Vars - if hasACLs, aclEnvs := AddACLsToEnv(solrConnectionInfo.CloudZkConnnectionInfo); hasACLs { + if hasACLs, aclEnvs := AddACLsToEnv(solrConnectionInfo.CloudZkConnnectionInfo.AllACL, solrConnectionInfo.CloudZkConnnectionInfo.ReadOnlyACL); hasACLs { envVars = append(envVars, aclEnvs...) // The $SOLR_ZK_CREDS_AND_ACLS parameter does not get picked up when running the Prometheus Exporter, it must be added to the JAVA_OPTS. diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go index 4288915..224291b 100644 --- a/controllers/util/solr_util.go +++ b/controllers/util/solr_util.go @@ -1206,7 +1206,8 @@ func createZkConnectionEnvVars(solrCloud *solr.SolrCloud, solrCloudStatus *solr. } // Add ACL information, if given, through Env Vars - if hasACLs, aclEnvs := AddACLsToEnv(solrCloud.Spec.ZookeeperRef.ConnectionInfo); hasACLs { + allACL, readOnlyACL := solrCloud.Spec.ZookeeperRef.GetACLs() + if hasACLs, aclEnvs := AddACLsToEnv(allACL, readOnlyACL); hasACLs { envVars = append(envVars, aclEnvs...) // The $SOLR_ZK_CREDS_AND_ACLS parameter does not get picked up when running solr, it must be added to the SOLR_OPTS. diff --git a/controllers/util/zk_util.go b/controllers/util/zk_util.go index 4241fd1..7e5ed40 100644 --- a/controllers/util/zk_util.go +++ b/controllers/util/zk_util.go @@ -85,6 +85,10 @@ func GenerateZookeeperCluster(solrCloud *solr.SolrCloud, zkSpec *solr.ZookeeperS zkCluster.Spec.Pod.NodeSelector = zkSpec.ZookeeperPod.NodeSelector } + if zkSpec.ZookeeperPod.Env != nil { + zkCluster.Spec.Pod.Env = zkSpec.ZookeeperPod.Env + } + if solrCloud.Spec.SolrAddressability.KubeDomain != "" { zkCluster.Spec.KubernetesClusterDomain = solrCloud.Spec.SolrAddressability.KubeDomain } @@ -172,6 +176,12 @@ func CopyZookeeperClusterFields(from, to *zk.ZookeeperCluster, logger logr.Logge to.Spec.Pod.Resources = from.Spec.Pod.Resources } + if !DeepEqualWithNils(to.Spec.Pod.Env, from.Spec.Pod.Env) { + logger.Info("Update required because field changed", "field", "Spec.Pod.Env", "from", to.Spec.Pod.Env, "to", from.Spec.Pod.Env) + requireUpdate = true + to.Spec.Pod.Env = from.Spec.Pod.Env + } + if !DeepEqualWithNils(to.Spec.Pod.Tolerations, from.Spec.Pod.Tolerations) { logger.Info("Update required because field changed", "field", "Spec.Pod.Tolerations", "from", to.Spec.Pod.Tolerations, "to", from.Spec.Pod.Tolerations) requireUpdate = true @@ -203,22 +213,23 @@ func CopyZookeeperClusterFields(from, to *zk.ZookeeperCluster, logger logr.Logge // AddACLsToEnv creates the neccessary environment variables for using ZK ACLs, and returns whether ACLs were provided. // info: Zookeeper Connection Information -func AddACLsToEnv(info *solr.ZookeeperConnectionInfo) (hasACLs bool, envVars []corev1.EnvVar) { - if info == nil || (info.AllACL == nil && info.ReadOnlyACL == nil) { +func AddACLsToEnv(allACL *solr.ZookeeperACL, readOnlyACL *solr.ZookeeperACL) (hasACLs bool, envVars []corev1.EnvVar) { + if allACL == nil && readOnlyACL == nil { return false, envVars } + f := false var zkDigests []string - if info.AllACL != nil { + if allACL != nil { envVars = append(envVars, corev1.EnvVar{ Name: "ZK_ALL_ACL_USERNAME", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: info.AllACL.SecretRef, + Name: allACL.SecretRef, }, - Key: info.AllACL.UsernameKey, + Key: allACL.UsernameKey, Optional: &f, }, }, @@ -228,25 +239,25 @@ func AddACLsToEnv(info *solr.ZookeeperConnectionInfo) (hasACLs bool, envVars []c ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: info.AllACL.SecretRef, + Name: allACL.SecretRef, }, - Key: info.AllACL.PasswordKey, + Key: allACL.PasswordKey, Optional: &f, }, }, }) zkDigests = append(zkDigests, "-DzkDigestUsername=$(ZK_ALL_ACL_USERNAME)", "-DzkDigestPassword=$(ZK_ALL_ACL_PASSWORD)") } - if info.ReadOnlyACL != nil { + if readOnlyACL != nil { envVars = append(envVars, corev1.EnvVar{ Name: "ZK_READ_ACL_USERNAME", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: info.ReadOnlyACL.SecretRef, + Name: readOnlyACL.SecretRef, }, - Key: info.ReadOnlyACL.UsernameKey, + Key: readOnlyACL.UsernameKey, Optional: &f, }, }, @@ -256,9 +267,9 @@ func AddACLsToEnv(info *solr.ZookeeperConnectionInfo) (hasACLs bool, envVars []c ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: info.ReadOnlyACL.SecretRef, + Name: readOnlyACL.SecretRef, }, - Key: info.ReadOnlyACL.PasswordKey, + Key: readOnlyACL.PasswordKey, Optional: &f, }, }, diff --git a/docs/solr-cloud/solr-cloud-crd.md b/docs/solr-cloud/solr-cloud-crd.md index e74f996..500d38f 100644 --- a/docs/solr-cloud/solr-cloud-crd.md +++ b/docs/solr-cloud/solr-cloud-crd.md @@ -130,7 +130,7 @@ All ACL fields are **required** if an ACL is used. - **`secret`** - The name of the secret, in the same namespace as the SolrCloud, that contains the admin ACL username and password. - **`usernameKey`** - The name of the key in the provided secret that stores the admin ACL username. -- **`usernameKey`** - The name of the key in the provided secret that stores the admin ACL password. +- **`passwordKey`** - The name of the key in the provided secret that stores the admin ACL password. ### Provided Instance @@ -142,6 +142,23 @@ each solrCloud that has this option specified. The startup parameter `zookeeper-operator` must be provided on startup of the solr-operator for this parameter to be available. +#### ACLs for Provided Ensembles +_Since v0.3.0_ + +If you want Solr to set ZK ACLs for znodes it creates in the `provided` ensemble, you can supply ACL credentials for an ADMIN and optionally a READ ONLY user using the following config settings: +- Admin: `SolrCloud.spec.zookeeperRef.provided.acl` +- Read Only: `SolrCloud.spec.zookeeperRef.provided.readOnlyAcl` + +All ACL fields are **required** if an ACL is used. + +- **`secret`** - The name of the secret, in the same namespace as the SolrCloud, that contains the ACL username and password. +- **`usernameKey`** - The name of the key in the provided secret that stores the admin ACL username. +- **`passwordKey`** - The name of the key in the provided secret that stores the admin ACL password. + +**Warning**: There is a known issue with the Zookeeper operator where it deploys pods with `skipACL=yes`, see: https://github.com/pravega/zookeeper-operator/issues/316. +This means that even if Solr sets the ACLs on znodes, they will not be enforced by Zookeeper. If your organization requires Solr to use ZK ACLs, then you'll need to +deploy Zookeeper to Kubernetes using another approach, such as using a Helm chart. + ## Override Built-in Solr Configuration Files _Since v0.2.7_ diff --git a/helm/solr-operator/crds/crds.yaml b/helm/solr-operator/crds/crds.yaml index 4149f6f..e027ced 100644 --- a/helm/solr-operator/crds/crds.yaml +++ b/helm/solr-operator/crds/crds.yaml @@ -5807,6 +5807,23 @@ spec: provided: description: 'Create a new Zookeeper Ensemble with the following spec Note: This option will not allow the SolrCloud to run across kube-clusters. Note: Requires - The zookeeperOperator flag to be provided to the Solr Operator - A zookeeper operator to be running' properties: + acl: + description: ZooKeeper ACL to use when connecting with ZK. This ACL should have ALL permission in the given chRoot. + properties: + passwordKey: + description: The name of the key in the given secret that contains the ACL password + type: string + secret: + description: The name of the Kubernetes Secret that stores the username and password for the ACL. This secret must be in the same namespace as the solrCloud or prometheusExporter is running in. + type: string + usernameKey: + description: The name of the key in the given secret that contains the ACL username + type: string + required: + - passwordKey + - secret + - usernameKey + type: object chroot: description: The ChRoot to connect solr at type: string @@ -5919,6 +5936,23 @@ spec: type: string type: object type: object + readOnlyAcl: + description: ZooKeeper ACL to use when connecting with ZK for reading operations. This ACL should have READ permission in the given chRoot. + properties: + passwordKey: + description: The name of the key in the given secret that contains the ACL password + type: string + secret: + description: The name of the Kubernetes Secret that stores the username and password for the ACL. This secret must be in the same namespace as the solrCloud or prometheusExporter is running in. + type: string + usernameKey: + description: The name of the key in the given secret that contains the ACL username + type: string + required: + - passwordKey + - secret + - usernameKey + type: object replicas: description: Number of members to create up for the ZK ensemble Defaults to 3 format: int32 @@ -6267,6 +6301,86 @@ spec: type: array type: object type: object + env: + description: List of environment variables to set in the main ZK container. + items: + description: EnvVar represents an environment variable present in a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) are expanded using the previous defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. The $(VAR_NAME) syntax can be escaped with a double $$, ie: $$(VAR_NAME). Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment variable's value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + 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 ConfigMap or its key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels[''<KEY>'']`, `metadata.annotations[''<KEY>'']`, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + 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 + type: object + required: + - name + type: object + type: array nodeSelector: additionalProperties: type: string