chore: new specs for defining service volumes

This commit is contained in:
Pavel Sviderski
2025-04-07 21:00:26 +10:00
parent 6125553a97
commit 9100c22e7f
2 changed files with 89 additions and 17 deletions
+26 -1
View File
@@ -46,6 +46,8 @@ type ServiceSpec struct {
Ports []PortSpec Ports []PortSpec
// Replicas is the number of containers to run for the service. Only valid for a replicated service. // Replicas is the number of containers to run for the service. Only valid for a replicated service.
Replicas uint `json:",omitempty"` Replicas uint `json:",omitempty"`
// Volumes is list of data volumes that can be mounted into the container.
Volumes []VolumeSpec
} }
func (s *ServiceSpec) SetDefaults() ServiceSpec { func (s *ServiceSpec) SetDefaults() ServiceSpec {
@@ -85,6 +87,20 @@ func (s *ServiceSpec) Validate() error {
// TODO: validate there is no conflict between ports. // 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 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. // PullPolicy determines when to pull the image from the registry or use the image already available in the cluster.
// Default is PullPolicyMissing if empty. // Default is PullPolicyMissing if empty.
PullPolicy string 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. // Volumes is list of data volumes to mount into the container.
// TODO: replace with []VolumeSpec // TODO: replace with []VolumeMounts
Volumes []string Volumes []string
} }
@@ -135,6 +154,12 @@ func (s *ContainerSpec) Validate() error {
return fmt.Errorf("invalid image: %w", err) 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 return nil
} }
+63 -16
View File
@@ -21,30 +21,77 @@ const (
SELinuxUnshared = "Z" SELinuxUnshared = "Z"
) )
// VolumeSpec defines a volume mount specification.
type VolumeSpec struct { type VolumeSpec struct {
Type string // Name is the volume name used to reference this volume in container mounts.
Source string Name string
Target string Type string
ReadOnly bool BindOptions *BindOptions `json:",omitempty"`
Bind *VolumeBind TmpfsOptions *mount.TmpfsOptions `json:",omitempty"`
// TODO: add options for tmpfs. VolumeOptions *VolumeOptions `json:",omitempty"`
} }
type VolumeBind struct { // BindOptions represents options for a bind volume.
CreateHostPath bool type BindOptions struct {
Propagation mount.Propagation // HostPath is the absolute path on the host filesystem.
SELinux string 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 { func (v *VolumeSpec) Validate() error {
switch v.Type { if v.Name == "" {
case VolumeTypeBind, VolumeTypeVolume: return fmt.Errorf("volume name must not be empty")
default:
return fmt.Errorf("invalid volume type: '%s'", v.Type)
} }
if !strings.HasPrefix(v.Target, "/") { switch v.Type {
return fmt.Errorf("invalid volume target: '%s', must be an absolute path in the container", v.Target) 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 return nil