From 9100c22e7faf43deca3393f1aded4abaa6466b97 Mon Sep 17 00:00:00 2001 From: Pavel Sviderski Date: Mon, 7 Apr 2025 21:00:26 +1000 Subject: [PATCH] chore: new specs for defining service volumes --- pkg/api/service.go | 27 +++++++++++++++- pkg/api/volume.go | 79 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 89 insertions(+), 17 deletions(-) diff --git a/pkg/api/service.go b/pkg/api/service.go index 3aa39005..231a4f89 100644 --- a/pkg/api/service.go +++ b/pkg/api/service.go @@ -46,6 +46,8 @@ type ServiceSpec struct { Ports []PortSpec // Replicas is the number of containers to run for the service. Only valid for a replicated service. Replicas uint `json:",omitempty"` + // Volumes is list of data volumes that can be mounted into the container. + Volumes []VolumeSpec } func (s *ServiceSpec) SetDefaults() ServiceSpec { @@ -85,6 +87,20 @@ func (s *ServiceSpec) Validate() error { // TODO: validate there is no conflict between ports. + for _, v := range s.Volumes { + if err := v.Validate(); err != nil { + return fmt.Errorf("invalid volume: %w", err) + } + } + for _, m := range s.Container.VolumeMounts { + if !slices.ContainsFunc(s.Volumes, func(v VolumeSpec) bool { + return v.Name == m.VolumeName + }) { + return fmt.Errorf("volume mount references a volume that doesn't exist in the service spec: '%s'", + m.VolumeName) + } + } + return nil } @@ -115,8 +131,11 @@ type ContainerSpec struct { // PullPolicy determines when to pull the image from the registry or use the image already available in the cluster. // Default is PullPolicyMissing if empty. PullPolicy string + // VolumeMounts specifies how volumes are mounted into the container filesystem. + // Each mount references a volume defined in ServiceSpec.Volumes. + VolumeMounts []VolumeMount // Volumes is list of data volumes to mount into the container. - // TODO: replace with []VolumeSpec + // TODO: replace with []VolumeMounts Volumes []string } @@ -135,6 +154,12 @@ func (s *ContainerSpec) Validate() error { return fmt.Errorf("invalid image: %w", err) } + for _, m := range s.VolumeMounts { + if err := m.Validate(); err != nil { + return fmt.Errorf("invalid volume mount: %w", err) + } + } + return nil } diff --git a/pkg/api/volume.go b/pkg/api/volume.go index f7c67c22..df66fd6f 100644 --- a/pkg/api/volume.go +++ b/pkg/api/volume.go @@ -21,30 +21,77 @@ const ( SELinuxUnshared = "Z" ) +// VolumeSpec defines a volume mount specification. type VolumeSpec struct { - Type string - Source string - Target string - ReadOnly bool - Bind *VolumeBind - // TODO: add options for tmpfs. + // Name is the volume name used to reference this volume in container mounts. + Name string + Type string + BindOptions *BindOptions `json:",omitempty"` + TmpfsOptions *mount.TmpfsOptions `json:",omitempty"` + VolumeOptions *VolumeOptions `json:",omitempty"` } -type VolumeBind struct { - CreateHostPath bool - Propagation mount.Propagation - SELinux string +// BindOptions represents options for a bind volume. +type BindOptions struct { + // HostPath is the absolute path on the host filesystem. + HostPath string + // AutoCreate indicates whether the host path should be created if it doesn't exist. + // If false, deployment will fail if the path doesn't exist. + AutoCreate bool `json:",omitempty"` + Propagation mount.Propagation `json:",omitempty"` + SELinux string `json:",omitempty"` +} + +// VolumeOptions represents options for a managed volume. +type VolumeOptions struct { + // AutoCreate indicates whether the volume should be created if it doesn't exist. + // If false, deployment will fail if the volume doesn't exist. + AutoCreate bool `json:",omitempty"` + // Driver specifies the volume driver and its options for volume creation (AutoCreate is true). + Driver *mount.Driver `json:",omitempty"` + // Labels are key-value metadata to apply to the volume if creating a new volume. + Labels map[string]string `json:",omitempty"` + // Name of the managed volume to use. If not specified, defaults to the VolumeSpec.Name. + Name string `json:",omitempty"` + // NoCopy prevents automatic copying of data from the container mount path to the volume. + NoCopy bool `json:",omitempty"` + // SubPath is the path within the volume to mount instead of its root. + SubPath string `json:",omitempty"` } func (v *VolumeSpec) Validate() error { - switch v.Type { - case VolumeTypeBind, VolumeTypeVolume: - default: - return fmt.Errorf("invalid volume type: '%s'", v.Type) + if v.Name == "" { + return fmt.Errorf("volume name must not be empty") } - if !strings.HasPrefix(v.Target, "/") { - return fmt.Errorf("invalid volume target: '%s', must be an absolute path in the container", v.Target) + switch v.Type { + case VolumeTypeBind, VolumeTypeVolume, VolumeTypeTmpfs: + default: + return fmt.Errorf("invalid volume type: '%s', must be one of '%s', '%s', '%s')", + v.Type, VolumeTypeBind, VolumeTypeVolume, VolumeTypeTmpfs) + } + + return nil +} + +// VolumeMount defines how a volume is mounted into a container. +type VolumeMount struct { + // VolumeName references a volume defined in ServiceSpec.Volumes by its Name field. + VolumeName string + // ContainerPath is the absolute path where the volume is mounted in the container. + ContainerPath string + // ReadOnly indicates whether the volume should be mounted read-only. + // If false (default), the volume is mounted read-write. + ReadOnly bool `json:",omitempty"` +} + +func (m *VolumeMount) Validate() error { + if m.VolumeName == "" { + return fmt.Errorf("volume name must not be empty") + } + + if !strings.HasPrefix(m.ContainerPath, "/") { + return fmt.Errorf("invalid container path: '%s', must be an absolute path in the container", m.ContainerPath) } return nil