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."