diff --git a/charts/opensearch-operator/files/opensearch.opster.io_opensearchclusters.yaml b/charts/opensearch-operator/files/opensearch.opster.io_opensearchclusters.yaml index 13385cd89..aeea8fb81 100644 --- a/charts/opensearch-operator/files/opensearch.opster.io_opensearchclusters.yaml +++ b/charts/opensearch-operator/files/opensearch.opster.io_opensearchclusters.yaml @@ -1283,6 +1283,29 @@ spec: name: description: Name to use for the volume. Required. type: string + nfs: + description: NFS object to use to populate the volume + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object path: description: Path in the container to mount the volume at. Required. @@ -1705,6 +1728,11 @@ spec: - name - path type: object + x-kubernetes-validations: + - message: exactly one of secret, configMap, emptyDir, csi, + projected, nfs must be set + rule: (has(self.secret)?1:0)+(has(self.configMap)?1:0)+(has(self.emptyDir)?1:0)+(has(self.csi)?1:0)+(has(self.projected)?1:0)+(has(self.nfs)?1:0) + == 1 type: array affinity: description: Affinity is a group of affinity scheduling rules. @@ -3557,6 +3585,29 @@ spec: name: description: Name to use for the volume. Required. type: string + nfs: + description: NFS object to use to populate the volume + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object path: description: Path in the container to mount the volume at. Required. @@ -3979,6 +4030,11 @@ spec: - name - path type: object + x-kubernetes-validations: + - message: exactly one of secret, configMap, emptyDir, csi, + projected, nfs must be set + rule: (has(self.secret)?1:0)+(has(self.configMap)?1:0)+(has(self.emptyDir)?1:0)+(has(self.csi)?1:0)+(has(self.projected)?1:0)+(has(self.nfs)?1:0) + == 1 type: array annotations: additionalProperties: diff --git a/docs/userguide/main.md b/docs/userguide/main.md index e744e14f4..8b359ac75 100644 --- a/docs/userguide/main.md +++ b/docs/userguide/main.md @@ -757,6 +757,12 @@ spec: sources: - serviceAccountToken: path: "token" + - name: nfs-volume + path: /mnt/backups/opensearch + nfs: + server: 192.168.1.233 + path: /export/backups/opensearch + readOnly: false # Optional, defaults to false dashboards: additionalVolumes: - name: example-secret @@ -765,6 +771,40 @@ spec: secretName: secret-name ``` +#### NFS Volume Support + +NFS volumes can be mounted directly into OpenSearch pods without requiring external provisioners or CSI drivers. This is particularly useful for snapshot repositories stored on NFS shares. To configure an NFS volume, specify the `nfs` field with the required `server` and `path` parameters: + +```yaml +spec: + general: + additionalVolumes: + - name: nfs-backups + path: /mnt/backups/opensearch + nfs: + server: 192.168.1.233 + path: /export/backups/opensearch + readOnly: false # Optional, defaults to false +``` + +This can be combined with snapshot repository configuration: + +```yaml +spec: + general: + additionalVolumes: + - name: nfs-backups + path: /mnt/backups/opensearch + nfs: + server: 192.168.1.233 + path: /export/backups/opensearch + snapshotRepositories: + - name: nfs-repository + type: fs + settings: + location: /mnt/backups/opensearch +``` + The defined volumes are added to all pods of the opensearch cluster. It is currently not possible to define them per nodepool. ### Adding environment variables to pods diff --git a/opensearch-operator/api/v1/opensearch_types.go b/opensearch-operator/api/v1/opensearch_types.go index e044660d4..424a39d8a 100644 --- a/opensearch-operator/api/v1/opensearch_types.go +++ b/opensearch-operator/api/v1/opensearch_types.go @@ -286,6 +286,7 @@ type ImageSpec struct { ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` } +// +kubebuilder:validation:XValidation:rule="(has(self.secret)?1:0)+(has(self.configMap)?1:0)+(has(self.emptyDir)?1:0)+(has(self.csi)?1:0)+(has(self.projected)?1:0)+(has(self.nfs)?1:0) == 1",message="exactly one of secret, configMap, emptyDir, csi, projected, nfs must be set" type AdditionalVolume struct { // Name to use for the volume. Required. Name string `json:"name"` @@ -303,6 +304,8 @@ type AdditionalVolume struct { CSI *corev1.CSIVolumeSource `json:"csi,omitempty"` // Projected object to use to populate the volume Projected *corev1.ProjectedVolumeSource `json:"projected,omitempty"` + // NFS object to use to populate the volume + NFS *corev1.NFSVolumeSource `json:"nfs,omitempty"` // Whether to restart the pods on content change RestartPods bool `json:"restartPods,omitempty"` } diff --git a/opensearch-operator/api/v1/zz_generated.deepcopy.go b/opensearch-operator/api/v1/zz_generated.deepcopy.go index 6594972f7..8273791cc 100644 --- a/opensearch-operator/api/v1/zz_generated.deepcopy.go +++ b/opensearch-operator/api/v1/zz_generated.deepcopy.go @@ -156,6 +156,11 @@ func (in *AdditionalVolume) DeepCopyInto(out *AdditionalVolume) { *out = new(corev1.ProjectedVolumeSource) (*in).DeepCopyInto(*out) } + if in.NFS != nil { + in, out := &in.NFS, &out.NFS + *out = new(corev1.NFSVolumeSource) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalVolume. diff --git a/opensearch-operator/config/crd/bases/opensearch.opster.io_opensearchclusters.yaml b/opensearch-operator/config/crd/bases/opensearch.opster.io_opensearchclusters.yaml index 13385cd89..aeea8fb81 100644 --- a/opensearch-operator/config/crd/bases/opensearch.opster.io_opensearchclusters.yaml +++ b/opensearch-operator/config/crd/bases/opensearch.opster.io_opensearchclusters.yaml @@ -1283,6 +1283,29 @@ spec: name: description: Name to use for the volume. Required. type: string + nfs: + description: NFS object to use to populate the volume + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object path: description: Path in the container to mount the volume at. Required. @@ -1705,6 +1728,11 @@ spec: - name - path type: object + x-kubernetes-validations: + - message: exactly one of secret, configMap, emptyDir, csi, + projected, nfs must be set + rule: (has(self.secret)?1:0)+(has(self.configMap)?1:0)+(has(self.emptyDir)?1:0)+(has(self.csi)?1:0)+(has(self.projected)?1:0)+(has(self.nfs)?1:0) + == 1 type: array affinity: description: Affinity is a group of affinity scheduling rules. @@ -3557,6 +3585,29 @@ spec: name: description: Name to use for the volume. Required. type: string + nfs: + description: NFS object to use to populate the volume + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object path: description: Path in the container to mount the volume at. Required. @@ -3979,6 +4030,11 @@ spec: - name - path type: object + x-kubernetes-validations: + - message: exactly one of secret, configMap, emptyDir, csi, + projected, nfs must be set + rule: (has(self.secret)?1:0)+(has(self.configMap)?1:0)+(has(self.emptyDir)?1:0)+(has(self.csi)?1:0)+(has(self.projected)?1:0)+(has(self.nfs)?1:0) + == 1 type: array annotations: additionalProperties: diff --git a/opensearch-operator/pkg/reconcilers/util/util.go b/opensearch-operator/pkg/reconcilers/util/util.go index ddf842462..550050944 100644 --- a/opensearch-operator/pkg/reconcilers/util/util.go +++ b/opensearch-operator/pkg/reconcilers/util/util.go @@ -146,13 +146,21 @@ func CreateAdditionalVolumes( }, }) } + if volumeConfig.NFS != nil { + retVolumes = append(retVolumes, corev1.Volume{ + Name: volumeConfig.Name, + VolumeSource: corev1.VolumeSource{ + NFS: volumeConfig.NFS, + }, + }) + } if volumeConfig.RestartPods { namesIndex[volumeConfig.Name] = i names = append(names, volumeConfig.Name) } subPath := "" - // SubPaths are only supported for ConfigMaps, Secrets and CSI volumes + // SubPaths are only supported for ConfigMaps, Secrets, CSI and Projected volumes if volumeConfig.ConfigMap != nil || volumeConfig.Secret != nil || volumeConfig.CSI != nil || volumeConfig.Projected != nil { subPath = strings.TrimSpace(volumeConfig.SubPath) } diff --git a/opensearch-operator/pkg/reconcilers/util/util_test.go b/opensearch-operator/pkg/reconcilers/util/util_test.go index 92dfefbfc..18571b0da 100644 --- a/opensearch-operator/pkg/reconcilers/util/util_test.go +++ b/opensearch-operator/pkg/reconcilers/util/util_test.go @@ -200,4 +200,23 @@ var _ = Describe("Additional volumes", func() { Expect(volumeMount[0].SubPath).To(BeEmpty()) }) }) + + When("NFS volume is added", func() { + It("Should have NFSVolumeSource fields and mount readOnly", func() { + volumeConfigs[0].NFS = &v1.NFSVolumeSource{ + Server: "10.0.0.1", + Path: "/export/path", + ReadOnly: true, + } + + volume, volumeMount, _, _ := CreateAdditionalVolumes(mockClient, namespace, volumeConfigs) + Expect(volume[0].NFS.Server).To(Equal("10.0.0.1")) + Expect(volume[0].NFS.Path).To(Equal("/export/path")) + Expect(volume[0].NFS.ReadOnly).To(BeTrue()) + Expect(volumeMount[0].MountPath).To(Equal("myPath/a/b")) + Expect(volumeMount[0].ReadOnly).To(BeTrue()) + Expect(volumeMount[0].SubPath).To(BeEmpty()) + + }) + }) })