Implement support for hostpath solr data volumes. (#265)
diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index 7aade22..34555d6 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -315,9 +315,14 @@
}
type SolrEphemeralDataStorageOptions struct {
+ // HostPathVolumeSource is an optional config to specify a path on the host machine to store Solr data.
+ //
+ // If hostPath is omitted, then the default EmptyDir is used, otherwise hostPath takes precedence over EmptyDir.
+ // +optional
+ HostPath *corev1.HostPathVolumeSource `json:"hostPath,omitempty"`
//EmptyDirVolumeSource is an optional config for the emptydir volume that will store Solr data.
// +optional
- EmptyDir corev1.EmptyDirVolumeSource `json:"emptyDir,omitempty"`
+ EmptyDir *corev1.EmptyDirVolumeSource `json:"emptyDir,omitempty"`
}
type SolrBackupRestoreOptions struct {
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index b3236f9..b7e44f4 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -887,7 +887,16 @@
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SolrEphemeralDataStorageOptions) DeepCopyInto(out *SolrEphemeralDataStorageOptions) {
*out = *in
- in.EmptyDir.DeepCopyInto(&out.EmptyDir)
+ if in.HostPath != nil {
+ in, out := &in.HostPath, &out.HostPath
+ *out = new(v1.HostPathVolumeSource)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.EmptyDir != nil {
+ in, out := &in.EmptyDir, &out.EmptyDir
+ *out = new(v1.EmptyDirVolumeSource)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SolrEphemeralDataStorageOptions.
diff --git a/config/crd/bases/solr.apache.org_solrclouds.yaml b/config/crd/bases/solr.apache.org_solrclouds.yaml
index 868f536..c6172e3 100644
--- a/config/crd/bases/solr.apache.org_solrclouds.yaml
+++ b/config/crd/bases/solr.apache.org_solrclouds.yaml
@@ -4338,6 +4338,18 @@
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
+ hostPath:
+ description: "HostPathVolumeSource is an optional config to specify a path on the host machine to store Solr data. \n If hostPath is omitted, then the default EmptyDir is used, otherwise hostPath takes precedence over EmptyDir."
+ properties:
+ path:
+ description: 'Path of the directory on the host. If the path is a symlink, it will follow the link to the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath'
+ type: string
+ type:
+ description: 'Type for HostPath Volume Defaults to "" More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath'
+ type: string
+ required:
+ - path
+ type: object
type: object
persistent:
description: "PersistentStorage is the specification for how the persistent Solr data storage should be configured. \n This option cannot be used with the \"ephemeral\" option."
diff --git a/controllers/solrcloud_controller_storage_test.go b/controllers/solrcloud_controller_storage_test.go
index b4eb422..d4169eb 100644
--- a/controllers/solrcloud_controller_storage_test.go
+++ b/controllers/solrcloud_controller_storage_test.go
@@ -18,9 +18,10 @@
package controllers
import (
+ "testing"
+
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
- "testing"
"github.com/apache/solr-operator/controllers/util"
"github.com/stretchr/testify/assert"
@@ -250,51 +251,10 @@
},
}
- // Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a
- // channel when it is finished.
- mgr, err := manager.New(testCfg, manager.Options{})
- g.Expect(err).NotTo(gomega.HaveOccurred())
- testClient = mgr.GetClient()
-
- solrCloudReconciler := &SolrCloudReconciler{
- Client: testClient,
- Log: ctrl.Log.WithName("controllers").WithName("SolrCloud"),
- }
- newRec, requests := SetupTestReconcile(solrCloudReconciler)
- g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
-
- stopMgr, mgrStopped := StartTestManager(mgr, g)
-
- defer func() {
- close(stopMgr)
- mgrStopped.Wait()
- }()
-
- cleanupTest(g, instance.Namespace)
-
- // Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
- err = testClient.Create(context.TODO(), instance)
- g.Expect(err).NotTo(gomega.HaveOccurred())
- defer testClient.Delete(context.TODO(), instance)
- g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
-
- // Fetch new value of instance to check finalizers
- foundInstance := &solr.SolrCloud{}
- g.Eventually(func() error {
- return testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, foundInstance)
- }, timeout).Should(gomega.Succeed())
- assert.Equal(t, 0, len(foundInstance.GetFinalizers()), "The solrcloud should have no finalizers when ephemeral storage is used")
-
- // Check the statefulSet
- statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
-
- assert.Equal(t, 3, len(statefulSet.Spec.Template.Spec.Volumes), "Pod has wrong number of volumes")
- assert.Equal(t, 0, len(statefulSet.Spec.VolumeClaimTemplates), "No data volume claims should exist when using ephemeral storage")
- dataVolume := statefulSet.Spec.Template.Spec.Volumes[1]
- assert.NotNil(t, dataVolume.EmptyDir, "The data volume should be an empty-dir.")
+ testForDefaultEmptyDirSpecs(t, g, instance)
}
-func TestEphemeralStorageWithSpecs(t *testing.T) {
+func TestDefaultEphemeralStorageWhenNilEmptyDir(t *testing.T) {
UseZkCRD(true)
g := gomega.NewGomegaWithT(t)
@@ -311,7 +271,42 @@
SolrLogLevel: "DEBUG",
StorageOptions: solr.SolrDataStorageOptions{
EphemeralStorage: &solr.SolrEphemeralDataStorageOptions{
- EmptyDir: corev1.EmptyDirVolumeSource{
+ EmptyDir: nil,
+ },
+ },
+ CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+ PodOptions: &solr.PodOptions{
+ EnvVariables: extraVars,
+ PodSecurityContext: &podSecurityContext,
+ Volumes: extraVolumes,
+ Affinity: affinity,
+ Resources: resources,
+ },
+ },
+ },
+ }
+
+ testForDefaultEmptyDirSpecs(t, g, instance)
+}
+
+func TestEphemeralStorageWithEmptyDirSpecs(t *testing.T) {
+ UseZkCRD(true)
+ g := gomega.NewGomegaWithT(t)
+
+ instance := &solr.SolrCloud{
+ ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+ Spec: solr.SolrCloudSpec{
+ ZookeeperRef: &solr.ZookeeperRef{
+ ConnectionInfo: &solr.ZookeeperConnectionInfo{
+ InternalConnectionString: "host:7271",
+ },
+ },
+ SolrJavaMem: "-Xmx4G",
+ SolrOpts: "extra-opts",
+ SolrLogLevel: "DEBUG",
+ StorageOptions: solr.SolrDataStorageOptions{
+ EphemeralStorage: &solr.SolrEphemeralDataStorageOptions{
+ EmptyDir: &corev1.EmptyDirVolumeSource{
Medium: corev1.StorageMediumMemory,
SizeLimit: resource.NewQuantity(1028*1028*1028, resource.BinarySI),
},
@@ -356,6 +351,8 @@
g.Expect(err).NotTo(gomega.HaveOccurred())
defer testClient.Delete(context.TODO(), instance)
g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
// Fetch new value of instance to check finalizers
foundInstance := &solr.SolrCloud{}
@@ -371,5 +368,185 @@
assert.Equal(t, 0, len(statefulSet.Spec.VolumeClaimTemplates), "No data volume claims should exist when using ephemeral storage")
dataVolume := statefulSet.Spec.Template.Spec.Volumes[1]
assert.NotNil(t, dataVolume.EmptyDir, "The data volume should be an empty-dir.")
- assert.EqualValues(t, instance.Spec.StorageOptions.EphemeralStorage.EmptyDir, *dataVolume.EmptyDir, "The empty dir settings do not match with what was provided.")
+ assert.Nil(t, dataVolume.HostPath, "The data volume should not be a hostPath volume.")
+ assert.EqualValues(t, instance.Spec.StorageOptions.EphemeralStorage.EmptyDir, dataVolume.EmptyDir, "The empty dir settings do not match with what was provided.")
+}
+
+func TestEphemeralStorageWithHostPathSpecs(t *testing.T) {
+ UseZkCRD(true)
+ g := gomega.NewGomegaWithT(t)
+
+ hostPathType := corev1.HostPathDirectoryOrCreate
+ instance := &solr.SolrCloud{
+ ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+ Spec: solr.SolrCloudSpec{
+ ZookeeperRef: &solr.ZookeeperRef{
+ ConnectionInfo: &solr.ZookeeperConnectionInfo{
+ InternalConnectionString: "host:7271",
+ },
+ },
+ SolrJavaMem: "-Xmx4G",
+ SolrOpts: "extra-opts",
+ SolrLogLevel: "DEBUG",
+ StorageOptions: solr.SolrDataStorageOptions{
+ EphemeralStorage: &solr.SolrEphemeralDataStorageOptions{
+ HostPath: &corev1.HostPathVolumeSource{
+ Path: "/tmp",
+ Type: &hostPathType,
+ },
+ },
+ },
+ CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+ PodOptions: &solr.PodOptions{
+ EnvVariables: extraVars,
+ PodSecurityContext: &podSecurityContext,
+ Volumes: extraVolumes,
+ Affinity: affinity,
+ Resources: resources,
+ },
+ },
+ },
+ }
+
+ testHostPathSpecs(t, g, instance)
+}
+
+func TestEphemeralStorageWithHostPathAndEmptyDirSpecs(t *testing.T) {
+ UseZkCRD(true)
+ g := gomega.NewGomegaWithT(t)
+
+ hostPathType := corev1.HostPathDirectoryOrCreate
+ instance := &solr.SolrCloud{
+ ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+ Spec: solr.SolrCloudSpec{
+ ZookeeperRef: &solr.ZookeeperRef{
+ ConnectionInfo: &solr.ZookeeperConnectionInfo{
+ InternalConnectionString: "host:7271",
+ },
+ },
+ SolrJavaMem: "-Xmx4G",
+ SolrOpts: "extra-opts",
+ SolrLogLevel: "DEBUG",
+ StorageOptions: solr.SolrDataStorageOptions{
+ EphemeralStorage: &solr.SolrEphemeralDataStorageOptions{
+ HostPath: &corev1.HostPathVolumeSource{
+ Path: "/tmp",
+ Type: &hostPathType,
+ },
+ EmptyDir: &corev1.EmptyDirVolumeSource{
+ Medium: corev1.StorageMediumMemory,
+ SizeLimit: resource.NewQuantity(1028*1028*1028, resource.BinarySI),
+ },
+ },
+ },
+ CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
+ PodOptions: &solr.PodOptions{
+ EnvVariables: extraVars,
+ PodSecurityContext: &podSecurityContext,
+ Volumes: extraVolumes,
+ Affinity: affinity,
+ Resources: resources,
+ },
+ },
+ },
+ }
+
+ testHostPathSpecs(t, g, instance)
+}
+
+func testHostPathSpecs(t *testing.T, g *gomega.GomegaWithT, instance *solr.SolrCloud) {
+
+ // Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a
+ // channel when it is finished.
+ mgr, err := manager.New(testCfg, manager.Options{})
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ testClient = mgr.GetClient()
+
+ solrCloudReconciler := &SolrCloudReconciler{
+ Client: testClient,
+ Log: ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+ }
+ newRec, requests := SetupTestReconcile(solrCloudReconciler)
+ g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+ stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+ defer func() {
+ close(stopMgr)
+ mgrStopped.Wait()
+ }()
+
+ cleanupTest(g, instance.Namespace)
+
+ // Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+ err = testClient.Create(context.TODO(), instance)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer testClient.Delete(context.TODO(), instance)
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+ // Fetch new value of instance to check finalizers
+ foundInstance := &solr.SolrCloud{}
+ g.Eventually(func() error {
+ return testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, foundInstance)
+ }, timeout).Should(gomega.Succeed())
+ assert.Equal(t, 0, len(foundInstance.GetFinalizers()), "The solrcloud should have no finalizers when ephemeral storage is used")
+
+ // Check the statefulSet
+ statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+ assert.Equal(t, 3, len(statefulSet.Spec.Template.Spec.Volumes), "Pod has wrong number of volumes")
+ assert.Equal(t, 0, len(statefulSet.Spec.VolumeClaimTemplates), "No data volume claims should exist when using ephemeral storage")
+ dataVolume := statefulSet.Spec.Template.Spec.Volumes[1]
+ assert.NotNil(t, dataVolume.HostPath, "The data volume should be a hostPath volume.")
+ assert.Nil(t, dataVolume.EmptyDir, "The data volume should not be an emptyDir volume.")
+ assert.EqualValues(t, instance.Spec.StorageOptions.EphemeralStorage.HostPath, dataVolume.HostPath, "The hostPath settings do not match with what was provided.")
+}
+
+func testForDefaultEmptyDirSpecs(t *testing.T, g *gomega.GomegaWithT, instance *solr.SolrCloud) {
+ // Setup the Manager and Controller. Wrap the Controller Reconcile function so it writes each request to a
+ // channel when it is finished.
+ mgr, err := manager.New(testCfg, manager.Options{})
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ testClient = mgr.GetClient()
+
+ solrCloudReconciler := &SolrCloudReconciler{
+ Client: testClient,
+ Log: ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+ }
+ newRec, requests := SetupTestReconcile(solrCloudReconciler)
+ g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+ stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+ defer func() {
+ close(stopMgr)
+ mgrStopped.Wait()
+ }()
+
+ cleanupTest(g, instance.Namespace)
+
+ // Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+ err = testClient.Create(context.TODO(), instance)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer testClient.Delete(context.TODO(), instance)
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+ g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+ // Fetch new value of instance to check finalizers
+ foundInstance := &solr.SolrCloud{}
+ g.Eventually(func() error {
+ return testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, foundInstance)
+ }, timeout).Should(gomega.Succeed())
+ assert.Equal(t, 0, len(foundInstance.GetFinalizers()), "The solrcloud should have no finalizers when ephemeral storage is used")
+
+ // Check the statefulSet
+ statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+ assert.Equal(t, 3, len(statefulSet.Spec.Template.Spec.Volumes), "Pod has wrong number of volumes")
+ assert.Equal(t, 0, len(statefulSet.Spec.VolumeClaimTemplates), "No data volume claims should exist when using ephemeral storage")
+ dataVolume := statefulSet.Spec.Template.Spec.Volumes[1]
+ assert.NotNil(t, dataVolume.EmptyDir, "The data volume should be an empty-dir.")
}
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index 508aabc..c8f3b05 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -214,16 +214,22 @@
},
}
} else {
- emptyDirVolume := corev1.Volume{
- Name: solrDataVolumeName,
- VolumeSource: corev1.VolumeSource{
- EmptyDir: &corev1.EmptyDirVolumeSource{},
- },
+ ephemeralVolume := corev1.Volume{
+ Name: solrDataVolumeName,
+ VolumeSource: corev1.VolumeSource{},
}
if solrCloud.Spec.StorageOptions.EphemeralStorage != nil {
- emptyDirVolume.VolumeSource.EmptyDir = &solrCloud.Spec.StorageOptions.EphemeralStorage.EmptyDir
+ if nil != solrCloud.Spec.StorageOptions.EphemeralStorage.HostPath {
+ ephemeralVolume.VolumeSource.HostPath = solrCloud.Spec.StorageOptions.EphemeralStorage.HostPath
+ } else if nil != solrCloud.Spec.StorageOptions.EphemeralStorage.EmptyDir {
+ ephemeralVolume.VolumeSource.EmptyDir = solrCloud.Spec.StorageOptions.EphemeralStorage.EmptyDir
+ } else {
+ ephemeralVolume.VolumeSource.EmptyDir = &corev1.EmptyDirVolumeSource{}
+ }
+ } else {
+ ephemeralVolume.VolumeSource.EmptyDir = &corev1.EmptyDirVolumeSource{}
}
- solrVolumes = append(solrVolumes, emptyDirVolume)
+ solrVolumes = append(solrVolumes, ephemeralVolume)
}
// Add backup volumes
if solrCloud.Spec.StorageOptions.BackupRestoreOptions != nil {
diff --git a/docs/solr-cloud/solr-cloud-crd.md b/docs/solr-cloud/solr-cloud-crd.md
index da7bcc3..9d0a049 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -28,8 +28,12 @@
Note: This template cannot be changed unless the SolrCloud is deleted and recreated.
This is a [limitation of StatefulSets and PVCs in Kubernetes](https://github.com/kubernetes/enhancements/issues/661).
- **`ephemeral`**
+
+ There are two types of ephemeral volumes that can be specified.
+ Both are optional, and if none are specified then an empty `emptyDir` volume source is used.
+ If both are specified then the `hostPath` volume source will take precedence.
- **`emptyDir`** - An [`emptyDir` volume source](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) that describes the desired emptyDir volume to use in each SolrCloud pod to store data.
- This option is optional, and if not provided an empty `emptyDir` volume source will be used.
+ - **`hostPath`** - A [`hostPath` volume source](https://kubernetes.io/docs/concepts/storage/volumes/#hostpath) that describes the desired hostPath volume to use in each SolrCloud pod to store data.
- **`backupRestoreOptions`** (Required for integration with [`SolrBackups`](../solr-backup/README.md))
- **`volume`** - This is a [volume source](https://kubernetes.io/docs/concepts/storage/volumes/), that supports `ReadWriteMany` access.
@@ -903,4 +907,4 @@
customSolrKubeOptions:
podOptions:
terminationGracePeriodSeconds: 120
-```
\ No newline at end of file
+```
diff --git a/helm/solr-operator/crds/crds.yaml b/helm/solr-operator/crds/crds.yaml
index d981efe..105a28b 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -5465,6 +5465,18 @@
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
type: object
+ hostPath:
+ description: "HostPathVolumeSource is an optional config to specify a path on the host machine to store Solr data. \n If hostPath is omitted, then the default EmptyDir is used, otherwise hostPath takes precedence over EmptyDir."
+ properties:
+ path:
+ description: 'Path of the directory on the host. If the path is a symlink, it will follow the link to the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath'
+ type: string
+ type:
+ description: 'Type for HostPath Volume Defaults to "" More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath'
+ type: string
+ required:
+ - path
+ type: object
type: object
persistent:
description: "PersistentStorage is the specification for how the persistent Solr data storage should be configured. \n This option cannot be used with the \"ephemeral\" option."