chore(volumes): map volume specs to container config, diff specs with volumes

This commit is contained in:
Pavel Sviderski
2025-04-09 21:51:20 +10:00
parent 13dea92311
commit c6205d4f57
5 changed files with 1024 additions and 63 deletions
+24
View File
@@ -62,6 +62,10 @@ func (s *ServiceSpec) SetDefaults() ServiceSpec {
}
spec.Container = spec.Container.SetDefaults()
for i, v := range spec.Volumes {
spec.Volumes[i] = v.SetDefaults()
}
return spec
}
@@ -87,11 +91,17 @@ func (s *ServiceSpec) Validate() error {
// TODO: validate there is no conflict between ports.
volumeNames := make(map[string]struct{})
for _, v := range s.Volumes {
if err := v.Validate(); err != nil {
return fmt.Errorf("invalid volume: %w", err)
}
if _, ok := volumeNames[v.Name]; ok {
return fmt.Errorf("duplicate volume name: '%s'", v.Name)
}
volumeNames[v.Name] = struct{}{}
}
for _, m := range s.Container.VolumeMounts {
if !slices.ContainsFunc(s.Volumes, func(v VolumeSpec) bool {
return v.Name == m.VolumeName
@@ -113,6 +123,13 @@ func (s *ServiceSpec) Clone() ServiceSpec {
}
spec.Container = s.Container.Clone()
if s.Volumes != nil {
spec.Volumes = make([]VolumeSpec, len(s.Volumes))
for i, v := range s.Volumes {
spec.Volumes[i] = v.Clone()
}
}
return spec
}
@@ -170,6 +187,9 @@ func (s *ContainerSpec) Equals(spec ContainerSpec) bool {
slices.Sort(orig.Volumes)
slices.Sort(spec.Volumes)
sortVolumeMounts(orig.VolumeMounts)
sortVolumeMounts(spec.VolumeMounts)
return reflect.DeepEqual(orig, spec)
}
@@ -188,6 +208,10 @@ func (s *ContainerSpec) Clone() ContainerSpec {
spec.Volumes = make([]string, len(s.Volumes))
copy(spec.Volumes, s.Volumes)
}
if s.VolumeMounts != nil {
spec.VolumeMounts = make([]VolumeMount, len(s.VolumeMounts))
copy(spec.VolumeMounts, s.VolumeMounts)
}
return spec
}
+101 -15
View File
@@ -2,6 +2,8 @@ package api
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/docker/docker/api/types/mount"
@@ -14,14 +16,10 @@ const (
VolumeTypeVolume = "volume"
// VolumeTypeTmpfs is the type for mounting a temporary file system stored in the host memory.
VolumeTypeTmpfs = "tmpfs"
// SELinuxShared share the volume content.
SELinuxShared = "z"
// SELinuxUnshared label content as private unshared.
SELinuxUnshared = "Z"
)
// VolumeSpec defines a volume mount specification.
// VolumeSpec defines a volume mount specification. As of April 2025, the volume must be created before deploying
// a service using it.
type VolumeSpec struct {
// Name is the volume name used to reference this volume in container mounts.
Name string
@@ -35,19 +33,16 @@ type VolumeSpec struct {
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.
// CreateHostPath 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"`
CreateHostPath bool `json:",omitempty"`
Propagation mount.Propagation `json:",omitempty"`
Recursive 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 specifies the volume driver and its options for volume creation.
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"`
@@ -59,13 +54,36 @@ type VolumeOptions struct {
SubPath string `json:",omitempty"`
}
func (v *VolumeSpec) SetDefaults() VolumeSpec {
spec := v.Clone()
if spec.Type == VolumeTypeVolume {
if spec.VolumeOptions == nil {
spec.VolumeOptions = &VolumeOptions{}
}
if spec.VolumeOptions.Driver == nil {
spec.VolumeOptions.Driver = &mount.Driver{Name: "local"}
}
if spec.VolumeOptions.Name == "" {
spec.VolumeOptions.Name = spec.Name
}
}
// TODO: set explicit default values for Propagation and Recursive for bind mounts?
return spec
}
func (v *VolumeSpec) Validate() error {
if v.Name == "" {
return fmt.Errorf("volume name must not be empty")
}
switch v.Type {
case VolumeTypeBind, VolumeTypeVolume, VolumeTypeTmpfs:
case VolumeTypeBind:
if v.BindOptions == nil {
return fmt.Errorf("bind volume must have bind options")
}
case VolumeTypeVolume, VolumeTypeTmpfs:
default:
return fmt.Errorf("invalid volume type: '%s', must be one of '%s', '%s', '%s')",
v.Type, VolumeTypeBind, VolumeTypeVolume, VolumeTypeTmpfs)
@@ -74,6 +92,57 @@ func (v *VolumeSpec) Validate() error {
return nil
}
func (v *VolumeSpec) Equals(other VolumeSpec) bool {
vol := v.SetDefaults()
other = other.SetDefaults()
return reflect.DeepEqual(vol, other)
}
func (v *VolumeSpec) Clone() VolumeSpec {
spec := *v
if v.BindOptions != nil {
opts := *v.BindOptions
spec.BindOptions = &opts
}
if v.VolumeOptions != nil {
opts := *v.VolumeOptions
if v.VolumeOptions.Driver != nil {
driver := *v.VolumeOptions.Driver
if driver.Options != nil {
driver.Options = make(map[string]string, len(v.VolumeOptions.Driver.Options))
for k, val := range v.VolumeOptions.Driver.Options {
driver.Options[k] = val
}
}
opts.Driver = &driver
}
if opts.Labels != nil {
opts.Labels = make(map[string]string, len(v.VolumeOptions.Labels))
for k, val := range v.VolumeOptions.Labels {
opts.Labels[k] = val
}
}
spec.VolumeOptions = &opts
}
if v.TmpfsOptions != nil {
opts := *v.TmpfsOptions
opts.Options = make([][]string, len(v.TmpfsOptions.Options))
for i, opt := range v.TmpfsOptions.Options {
opts.Options[i] = make([]string, len(opt))
copy(opts.Options[i], opt)
}
spec.TmpfsOptions = &opts
}
return spec
}
// 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.
@@ -96,3 +165,20 @@ func (m *VolumeMount) Validate() error {
return nil
}
func sortVolumeMounts(mounts []VolumeMount) {
sort.Slice(mounts, func(i, j int) bool {
if mounts[i].VolumeName != mounts[j].VolumeName {
return mounts[i].VolumeName < mounts[j].VolumeName
}
if mounts[i].ContainerPath != mounts[j].ContainerPath {
return mounts[i].ContainerPath < mounts[j].ContainerPath
}
if mounts[i].ReadOnly != mounts[j].ReadOnly {
if !mounts[i].ReadOnly {
return true
}
}
return false
})
}