diff --git a/config/marshal_config_test.go b/config/marshal_config_test.go index 5b807584f..0bfefbfd6 100644 --- a/config/marshal_config_test.go +++ b/config/marshal_config_test.go @@ -46,14 +46,37 @@ func newTestConfig() TestConfig { "io.rancher.os.createonly": "true", "io.rancher.os.scope": "system", }, - Volumes: []string{ - "/dev:/host/dev", - "/var/lib/rancher/conf:/var/lib/rancher/conf", - "/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt.rancher", - "/lib/modules:/lib/modules", - "/lib/firmware:/lib/firmware", - "/var/run:/var/run", - "/var/log:/var/log", + Volumes: &yamlTypes.Volumes{ + Volumes: []*yamlTypes.Volume{ + { + Source: "/dev", + Destination: "/host/dev", + }, + { + Source: "/var/lib/rancher/conf", + Destination: "/var/lib/rancher/conf", + }, + { + Source: "/etc/ssl/certs/ca-certificates.crt", + Destination: "/etc/ssl/certs/ca-certificates.crt.rancher", + }, + { + Source: "/lib/modules", + Destination: "lib/modules", + }, + { + Source: "/lib/firmware", + Destination: "/lib/firmware", + }, + { + Source: "/var/run", + Destination: "/var/run", + }, + { + Source: "/var/log", + Destination: "/var/log", + }, + }, }, Logging: Log{ Driver: "json-file", diff --git a/config/types.go b/config/types.go index 2de64f4a1..516c0a387 100644 --- a/config/types.go +++ b/config/types.go @@ -116,7 +116,7 @@ type ServiceConfig struct { ShmSize yaml.StringorInt `yaml:"shm_size,omitempty"` StopSignal string `yaml:"stop_signal,omitempty"` VolumeDriver string `yaml:"volume_driver,omitempty"` - Volumes []string `yaml:"volumes,omitempty"` + Volumes *yaml.Volumes `yaml:"volumes,omitempty"` VolumesFrom []string `yaml:"volumes_from,omitempty"` Uts string `yaml:"uts,omitempty"` Restart string `yaml:"restart,omitempty"` diff --git a/docker/convert.go b/docker/convert.go index baed9a477..fd7375b7c 100644 --- a/docker/convert.go +++ b/docker/convert.go @@ -16,6 +16,7 @@ import ( composeclient "github.com/docker/libcompose/docker/client" "github.com/docker/libcompose/project" "github.com/docker/libcompose/utils" + // "github.com/docker/libcompose/yaml" ) // ConfigWrapper wraps Config, HostConfig and NetworkingConfig for a container. @@ -36,6 +37,16 @@ func Filter(vs []string, f func(string) bool) []string { return r } +func toMap(vs []string) map[string]struct{} { + m := map[string]struct{}{} + for _, v := range vs { + if v != "" { + m[v] = struct{}{} + } + } + return m +} + func isBind(s string) bool { return strings.ContainsRune(s, ':') } @@ -58,21 +69,18 @@ func ConvertToAPI(serviceConfig *config.ServiceConfig, ctx project.Context, clie return &result, nil } -func isNamedVolume(volume string) bool { - return !strings.HasPrefix(volume, ".") && !strings.HasPrefix(volume, "/") && !strings.HasPrefix(volume, "~") -} - -func volumes(c *config.ServiceConfig, ctx project.Context) map[string]struct{} { - volumes := make(map[string]struct{}, len(c.Volumes)) - for k, v := range c.Volumes { - if len(ctx.ComposeFiles) > 0 && !isNamedVolume(v) { - v = ctx.ResourceLookup.ResolvePath(v, ctx.ComposeFiles[0]) - } - - c.Volumes[k] = v - if isVolume(v) { - volumes[v] = struct{}{} +func volumes(c *config.ServiceConfig, ctx project.Context) []string { + if c.Volumes == nil { + return []string{} + } + volumes := make([]string, len(c.Volumes.Volumes)) + for _, v := range c.Volumes.Volumes { + vol := v + if len(ctx.ComposeFiles) > 0 && !project.IsNamedVolume(v.Source) { + sourceVol := ctx.ResourceLookup.ResolvePath(v.String(), ctx.ComposeFiles[0]) + vol.Source = strings.SplitN(sourceVol, ":", 2)[0] } + volumes = append(volumes, vol.String()) } return volumes } @@ -141,6 +149,8 @@ func Convert(c *config.ServiceConfig, ctx project.Context, clientFactory compose } } + vols := volumes(c, ctx) + config := &container.Config{ Entrypoint: strslice.StrSlice(utils.CopySlice(c.Entrypoint)), Hostname: c.Hostname, @@ -154,7 +164,7 @@ func Convert(c *config.ServiceConfig, ctx project.Context, clientFactory compose Tty: c.Tty, OpenStdin: c.StdinOpen, WorkingDir: c.WorkingDir, - Volumes: volumes(c, ctx), + Volumes: toMap(Filter(vols, isVolume)), MacAddress: c.MacAddress, } @@ -228,7 +238,7 @@ func Convert(c *config.ServiceConfig, ctx project.Context, clientFactory compose CapDrop: strslice.StrSlice(utils.CopySlice(c.CapDrop)), ExtraHosts: utils.CopySlice(c.ExtraHosts), Privileged: c.Privileged, - Binds: Filter(c.Volumes, isBind), + Binds: Filter(vols, isBind), DNS: utils.CopySlice(c.DNS), DNSSearch: utils.CopySlice(c.DNSSearch), LogConfig: container.LogConfig{ diff --git a/docker/convert_test.go b/docker/convert_test.go index e3442e06b..cbaefa8ba 100644 --- a/docker/convert_test.go +++ b/docker/convert_test.go @@ -26,7 +26,29 @@ func TestParseBindsAndVolumes(t *testing.T) { abs, err := filepath.Abs(".") assert.Nil(t, err) cfg, hostCfg, err := Convert(&config.ServiceConfig{ - Volumes: []string{"/foo", "/home:/home", "/bar/baz", ".:/home", "/usr/lib:/usr/lib:ro"}, + Volumes: &yaml.Volumes{ + Volumes: []*yaml.Volume{ + { + Destination: "/foo", + }, + { + Source: "/home", + Destination: "/home", + }, + { + Destination: "/bar/baz", + }, + { + Source: ".", + Destination: "/home", + }, + { + Source: "/usr/lib", + Destination: "/usr/lib", + AccessMode: "ro", + }, + }, + }, }, ctx.Context, nil) assert.Nil(t, err) assert.Equal(t, map[string]struct{}{"/foo": {}, "/bar/baz": {}}, cfg.Volumes) diff --git a/docker/project.go b/docker/project.go index b356c436d..a3aa8fa3f 100644 --- a/docker/project.go +++ b/docker/project.go @@ -12,6 +12,7 @@ import ( "github.com/docker/libcompose/config" "github.com/docker/libcompose/docker/client" "github.com/docker/libcompose/docker/network" + "github.com/docker/libcompose/docker/volume" "github.com/docker/libcompose/labels" "github.com/docker/libcompose/lookup" "github.com/docker/libcompose/project" @@ -66,6 +67,13 @@ func NewProject(context *Context, parseOptions *config.ParseOptions) (project.AP context.NetworksFactory = networksFactory } + if context.VolumesFactory == nil { + volumesFactory := &volume.DockerFactory{ + ClientFactory: context.ClientFactory, + } + context.VolumesFactory = volumesFactory + } + // FIXME(vdemeester) Remove the context duplication ? runtime := &Project{ clientFactory: context.ClientFactory, diff --git a/docker/volume/volume.go b/docker/volume/volume.go new file mode 100644 index 000000000..427828425 --- /dev/null +++ b/docker/volume/volume.go @@ -0,0 +1,154 @@ +package volume + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/engine-api/client" + "github.com/docker/engine-api/types" + "github.com/docker/libcompose/config" + + composeclient "github.com/docker/libcompose/docker/client" + "github.com/docker/libcompose/project" +) + +// Volume holds attributes and method for a volume definition in compose +type Volume struct { + client client.VolumeAPIClient + projectName string + name string + driver string + driverOptions map[string]string + external bool + // TODO (shouze) missing labels +} + +func (v *Volume) fullName() string { + name := v.projectName + "_" + v.name + if v.external { + name = v.name + } + return name +} + +// Inspect inspect the current volume +func (v *Volume) Inspect(ctx context.Context) (types.Volume, error) { + return v.client.VolumeInspect(ctx, v.fullName()) +} + +// Remove removes the current volume (from docker engine) +func (v *Volume) Remove(ctx context.Context) error { + if v.external { + fmt.Printf("Volume %s is external, skipping", v.fullName()) + return nil + } + fmt.Printf("Removing volume %q\n", v.fullName()) + return v.client.VolumeRemove(ctx, v.fullName()) +} + +// EnsureItExists make sure the volume exists and return an error if it does not exists +// and cannot be created. +func (v *Volume) EnsureItExists(ctx context.Context) error { + volumeResource, err := v.Inspect(ctx) + if v.external { + if client.IsErrVolumeNotFound(err) { + // FIXME(shouze) introduce some libcompose error type + return fmt.Errorf("Volume %s declared as external, but could not be found. Please create the volume manually using docker volume create %s and try again", v.name, v.name) + } + return err + } + if err != nil && client.IsErrVolumeNotFound(err) { + return v.create(ctx) + } + if volumeResource.Driver != v.driver { + return fmt.Errorf("Volume %q needs to be recreated - driver has changed", v.name) + } + return err +} + +func (v *Volume) create(ctx context.Context) error { + fmt.Printf("Creating volume %q with driver %q\n", v.fullName(), v.driver) + _, err := v.client.VolumeCreate(ctx, types.VolumeCreateRequest{ + Name: v.fullName(), + Driver: v.driver, + DriverOpts: v.driverOptions, + // TODO (shouze) missing labels + }) + + return err +} + +// NewVolume creates a new volume from the specified name and config. +func NewVolume(projectName, name string, config *config.VolumeConfig, client client.VolumeAPIClient) *Volume { + return &Volume{ + client: client, + projectName: projectName, + name: name, + driver: config.Driver, + driverOptions: config.DriverOpts, + external: config.External.External, + } +} + +// Volumes holds a list of volume +type Volumes struct { + volumes []*Volume + volumeEnabled bool +} + +// Initialize make sure volume exists if volume is enabled +func (v *Volumes) Initialize(ctx context.Context) error { + if !v.volumeEnabled { + return nil + } + for _, volume := range v.volumes { + err := volume.EnsureItExists(ctx) + if err != nil { + return err + } + } + return nil +} + +// Remove removes volumes (clean-up) +func (v *Volumes) Remove(ctx context.Context) error { + if !v.volumeEnabled { + return nil + } + for _, volume := range v.volumes { + err := volume.Remove(ctx) + if err != nil { + return err + } + } + return nil +} + +// VolumesFromServices creates a new Volumes struct based on volumes configurations and +// services configuration. If a volume is defined but not used by any service, it will return +// an error along the Volumes. +func VolumesFromServices(cli client.VolumeAPIClient, projectName string, volumeConfigs map[string]*config.VolumeConfig, services *config.ServiceConfigs, volumeEnabled bool) (*Volumes, error) { + var err error + volumes := make([]*Volume, 0, len(volumeConfigs)) + for name, config := range volumeConfigs { + volume := NewVolume(projectName, name, config, cli) + volumes = append(volumes, volume) + } + return &Volumes{ + volumes: volumes, + volumeEnabled: volumeEnabled, + }, err +} + +// DockerFactory implements project.VolumesFactory +type DockerFactory struct { + ClientFactory composeclient.Factory +} + +// Create implements project.VolumesFactory Create method. +// It creates a Volumes (that implements project.Volumes) from specified configurations. +func (f *DockerFactory) Create(projectName string, volumeConfigs map[string]*config.VolumeConfig, serviceConfigs *config.ServiceConfigs, volumeEnabled bool) (project.Volumes, error) { + cli := f.ClientFactory.Create(nil) + return VolumesFromServices(cli, projectName, volumeConfigs, serviceConfigs, volumeEnabled) +} diff --git a/docker/volume/volume_test.go b/docker/volume/volume_test.go new file mode 100644 index 000000000..4f669a8d1 --- /dev/null +++ b/docker/volume/volume_test.go @@ -0,0 +1,84 @@ +package volume + +import ( + "testing" + + "github.com/docker/engine-api/types" + "github.com/docker/libcompose/config" + "github.com/docker/libcompose/test" +) + +func TestVolumesFromServices(t *testing.T) { + cases := []struct { + volumeConfigs map[string]*config.VolumeConfig + services map[string]*config.ServiceConfig + volumeEnabled bool + expectedVolumes []*Volume + expectedError bool + }{ + {}, + { + volumeConfigs: map[string]*config.VolumeConfig{ + "vol1": {}, + }, + services: map[string]*config.ServiceConfig{}, + expectedVolumes: []*Volume{ + { + name: "vol1", + projectName: "prj", + }, + }, + expectedError: false, + }, + } + + for index, c := range cases { + services := config.NewServiceConfigs() + for name, service := range c.services { + services.Add(name, service) + } + + volumes, err := VolumesFromServices(&volumeClient{}, "prj", c.volumeConfigs, services, c.volumeEnabled) + if c.expectedError { + if err == nil { + t.Fatalf("%d: expected an error, got nothing", index) + } + } else { + if err != nil { + t.Fatalf("%d: didn't expect an error, got one %s", index, err.Error()) + } + } + if volumes.volumeEnabled != c.volumeEnabled { + t.Fatalf("%d: expected volume enabled %v, got %v", index, c.volumeEnabled, volumes.volumeEnabled) + } + if len(volumes.volumes) != len(c.expectedVolumes) { + t.Fatalf("%d: expected %v, got %v", index, c.expectedVolumes, volumes.volumes) + } + for _, volume := range volumes.volumes { + testExpectedContainsVolume(t, index, c.expectedVolumes, volume) + } + } +} + +func testExpectedContainsVolume(t *testing.T, index int, expected []*Volume, volume *Volume) { + found := false + for _, e := range expected { + if e.name == volume.name { + found = true + break + } + } + if !found { + t.Fatalf("%d: volume %v not found in %v", index, volume, expected) + } +} + +type volumeClient struct { + test.NopClient + expectedName string + expectedVolumeCreate types.VolumeCreateRequest + inspectError error + inspectVolumeDriver string + inspectVolumeOptions map[string]string + removeError error +} diff --git a/integration/common_test.go b/integration/common_test.go index 6bd2aa46d..ff39916ec 100644 --- a/integration/common_test.go +++ b/integration/common_test.go @@ -167,6 +167,15 @@ func (s *CliSuite) GetContainerByName(c *C, name string) *types.ContainerJSON { return container } +func (s *CliSuite) GetVolumeByName(c *C, name string) *types.Volume { + client := GetClient(c) + volume, err := client.VolumeInspect(context.Background(), name) + + c.Assert(err, IsNil) + + return &volume +} + func (s *CliSuite) GetContainersByProject(c *C, project string) []types.Container { client := GetClient(c) containers, err := docker.GetContainersByFilter(context.Background(), client, labels.PROJECT.Eq(project)) diff --git a/integration/volume_test.go b/integration/volume_test.go index 9cf2131a6..e91a81958 100644 --- a/integration/volume_test.go +++ b/integration/volume_test.go @@ -75,3 +75,23 @@ func (s *CliSuite) TestNamedVolume(c *C) { c.Assert(cn.Mounts[0].Name, DeepEquals, "vol") c.Assert(cn.Mounts[0].Destination, DeepEquals, "/path") } + +func (s *CliSuite) TestV2Volume(c *C) { + p := s.ProjectFromText(c, "up", `version: "2" +services: + with_volume: + image: busybox + volumes: + - test:/test + +volumes: + test: {} + test2: {} +`) + + v := s.GetVolumeByName(c, p+"_test") + c.Assert(v, NotNil) + + v = s.GetVolumeByName(c, p+"_test2") + c.Assert(v, NotNil) +} diff --git a/project/context.go b/project/context.go index 7ac69543b..07d6f194b 100644 --- a/project/context.go +++ b/project/context.go @@ -25,6 +25,7 @@ type Context struct { isOpen bool ServiceFactory ServiceFactory NetworksFactory NetworksFactory + VolumesFactory VolumesFactory EnvironmentLookup config.EnvironmentLookup ResourceLookup config.ResourceLookup LoggerFactory logger.Factory diff --git a/project/project.go b/project/project.go index 170851b13..e9a12bd95 100644 --- a/project/project.go +++ b/project/project.go @@ -31,6 +31,7 @@ type Project struct { runtime RuntimeProject networks Networks + volumes Volumes configVersion string context *Context reload []string @@ -205,6 +206,31 @@ func (p *Project) load(file string, bytes []byte) error { } // Update network configuration a little bit + p.handleNetworkConfig() + p.handleVolumeConfig() + + if p.context.NetworksFactory != nil { + networks, err := p.context.NetworksFactory.Create(p.Name, p.NetworkConfigs, p.ServiceConfigs, p.isNetworkEnabled()) + if err != nil { + return err + } + + p.networks = networks + } + + if p.context.VolumesFactory != nil { + volumes, err := p.context.VolumesFactory.Create(p.Name, p.VolumeConfigs, p.ServiceConfigs, p.isVolumeEnabled()) + if err != nil { + return err + } + + p.volumes = volumes + } + + return nil +} + +func (p *Project) handleNetworkConfig() { if p.isNetworkEnabled() { for _, serviceName := range p.ServiceConfigs.Keys() { serviceConfig, _ := p.ServiceConfigs.Get(serviceName) @@ -238,21 +264,44 @@ func (p *Project) load(file string, bytes []byte) error { } } } +} - // FIXME(vdemeester) Not sure about this.. - if p.context.NetworksFactory != nil { - networks, err := p.context.NetworksFactory.Create(p.Name, p.NetworkConfigs, p.ServiceConfigs, p.isNetworkEnabled()) - if err != nil { - return err - } +func (p *Project) isNetworkEnabled() bool { + return p.configVersion == "2" +} - p.networks = networks - } +func (p *Project) handleVolumeConfig() { + if p.isVolumeEnabled() { + for _, serviceName := range p.ServiceConfigs.Keys() { + serviceConfig, _ := p.ServiceConfigs.Get(serviceName) + // Consolidate the name of the volume + // FIXME(vdemeester) probably shouldn't be there, maybe move that to interface/factory + if serviceConfig.Volumes == nil { + continue + } + for _, volume := range serviceConfig.Volumes.Volumes { + if !IsNamedVolume(volume.Source) { + continue + } - return nil + vol, ok := p.VolumeConfigs[volume.Source] + if !ok { + continue + } + + if vol.External.External { + if vol.External.Name != "" { + volume.Source = vol.External.Name + } + } else { + volume.Source = p.Name + "_" + volume.Source + } + } + } + } } -func (p *Project) isNetworkEnabled() bool { +func (p *Project) isVolumeEnabled() bool { return p.configVersion == "2" } @@ -264,7 +313,11 @@ func (p *Project) initialize(ctx context.Context) error { return err } } - // TODO Initialize volumes + if p.volumes != nil { + if err := p.volumes.Initialize(ctx); err != nil { + return err + } + } return nil } @@ -337,6 +390,16 @@ func (p *Project) Down(ctx context.Context, opts options.Down, services ...strin return err } + if opts.RemoveVolume { + volumes, err := p.context.VolumesFactory.Create(p.Name, p.VolumeConfigs, p.ServiceConfigs, p.isVolumeEnabled()) + if err != nil { + return err + } + if err := volumes.Remove(ctx); err != nil { + return err + } + } + return p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) { wrapper.Do(wrappers, events.NoEvent, events.NoEvent, func(service Service) error { return service.RemoveImage(ctx, opts.RemoveImages) @@ -746,3 +809,8 @@ func (p *Project) Notify(eventType events.EventType, serviceName string, data ma l <- event } } + +// IsNamedVolume returns whether the specified volume (string) is a named volume or not. +func IsNamedVolume(volume string) bool { + return !strings.HasPrefix(volume, ".") && !strings.HasPrefix(volume, "/") && !strings.HasPrefix(volume, "~") +} diff --git a/project/volume.go b/project/volume.go new file mode 100644 index 000000000..08f926e30 --- /dev/null +++ b/project/volume.go @@ -0,0 +1,19 @@ +package project + +import ( + "golang.org/x/net/context" + + "github.com/docker/libcompose/config" +) + +// Volumes defines the methods a libcompose volume aggregate should define. +type Volumes interface { + Initialize(ctx context.Context) error + Remove(ctx context.Context) error +} + +// VolumesFactory is an interface factory to create Volumes object for the specified +// configurations (service, volumes, …) +type VolumesFactory interface { + Create(projectName string, volumeConfigs map[string]*config.VolumeConfig, serviceConfigs *config.ServiceConfigs, volumeEnabled bool) (Volumes, error) +} diff --git a/yaml/volume.go b/yaml/volume.go new file mode 100644 index 000000000..530aa6179 --- /dev/null +++ b/yaml/volume.go @@ -0,0 +1,83 @@ +package yaml + +import ( + "errors" + "fmt" + "strings" +) + +// Volumes represents a list of service volumes in compose file. +// It has several representation, hence this specific struct. +type Volumes struct { + Volumes []*Volume +} + +// Volume represent a service volume +type Volume struct { + Source string `yaml:"-"` + Destination string `yaml:"-"` + AccessMode string `yaml:"-"` +} + +// String implements the Stringer interface. +func (v *Volume) String() string { + var paths []string + if v.Source != "" { + paths = []string{v.Source, v.Destination} + } else { + paths = []string{v.Destination} + } + if v.AccessMode != "" { + paths = append(paths, v.AccessMode) + } + return strings.Join(paths, ":") +} + +// MarshalYAML implements the Marshaller interface. +func (v Volumes) MarshalYAML() (interface{}, error) { + vs := []string{} + for _, volume := range v.Volumes { + vs = append(vs, volume.String()) + } + return vs, nil +} + +// UnmarshalYAML implements the Unmarshaller interface. +func (v *Volumes) UnmarshalYAML(unmarshal func(interface{}) error) error { + var sliceType []interface{} + if err := unmarshal(&sliceType); err == nil { + v.Volumes = []*Volume{} + for _, volume := range sliceType { + name, ok := volume.(string) + if !ok { + return fmt.Errorf("Cannot unmarshal '%v' to type %T into a string value", name, name) + } + elts := strings.SplitN(name, ":", 3) + var vol *Volume + switch { + case len(elts) == 1: + vol = &Volume{ + Destination: elts[0], + } + case len(elts) == 2: + vol = &Volume{ + Source: elts[0], + Destination: elts[1], + } + case len(elts) == 3: + vol = &Volume{ + Source: elts[0], + Destination: elts[1], + AccessMode: elts[2], + } + default: + // FIXME + return fmt.Errorf("") + } + v.Volumes = append(v.Volumes, vol) + } + return nil + } + + return errors.New("Failed to unmarshal Volumes") +} diff --git a/yaml/volume_test.go b/yaml/volume_test.go new file mode 100644 index 000000000..b78ef7a7c --- /dev/null +++ b/yaml/volume_test.go @@ -0,0 +1,143 @@ +package yaml + +import ( + "testing" + + "gopkg.in/yaml.v2" + + "github.com/stretchr/testify/assert" +) + +func TestMarshalVolumes(t *testing.T) { + volumes := []struct { + volumes Volumes + expected string + }{ + { + volumes: Volumes{}, + expected: `[] +`, + }, + { + volumes: Volumes{ + Volumes: []*Volume{ + { + Destination: "/in/the/container", + }, + }, + }, + expected: `- /in/the/container +`, + }, + { + volumes: Volumes{ + Volumes: []*Volume{ + { + Source: "./a/path", + Destination: "/in/the/container", + AccessMode: "ro", + }, + }, + }, + expected: `- ./a/path:/in/the/container:ro +`, + }, + { + volumes: Volumes{ + Volumes: []*Volume{ + { + Source: "./a/path", + Destination: "/in/the/container", + }, + }, + }, + expected: `- ./a/path:/in/the/container +`, + }, + { + volumes: Volumes{ + Volumes: []*Volume{ + { + Source: "./a/path", + Destination: "/in/the/container", + }, + { + Source: "named", + Destination: "/in/the/container", + }, + }, + }, + expected: `- ./a/path:/in/the/container +- named:/in/the/container +`, + }, + } + for _, volume := range volumes { + bytes, err := yaml.Marshal(volume.volumes) + assert.Nil(t, err) + assert.Equal(t, volume.expected, string(bytes), "should be equal") + } +} + +func TestUnmarshalVolumes(t *testing.T) { + volumes := []struct { + yaml string + expected *Volumes + }{ + { + yaml: `- ./a/path:/in/the/container`, + expected: &Volumes{ + Volumes: []*Volume{ + { + Source: "./a/path", + Destination: "/in/the/container", + }, + }, + }, + }, + { + yaml: `- /in/the/container`, + expected: &Volumes{ + Volumes: []*Volume{ + { + Destination: "/in/the/container", + }, + }, + }, + }, + { + yaml: `- /a/path:/in/the/container:ro`, + expected: &Volumes{ + Volumes: []*Volume{ + { + Source: "/a/path", + Destination: "/in/the/container", + AccessMode: "ro", + }, + }, + }, + }, + { + yaml: `- /a/path:/in/the/container +- named:/somewhere/in/the/container`, + expected: &Volumes{ + Volumes: []*Volume{ + { + Source: "/a/path", + Destination: "/in/the/container", + }, + { + Source: "named", + Destination: "/somewhere/in/the/container", + }, + }, + }, + }, + } + for _, volume := range volumes { + actual := &Volumes{} + err := yaml.Unmarshal([]byte(volume.yaml), actual) + assert.Nil(t, err) + assert.Equal(t, volume.expected, actual, "should be equal") + } +}