Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 4dc7080

Browse files
authored
Merge pull request #339 from vdemeester/v2-volumes
Add support to volume v2 🐡
2 parents c2bad78 + ae73652 commit 4dc7080

File tree

14 files changed

+681
-37
lines changed

14 files changed

+681
-37
lines changed

config/marshal_config_test.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,37 @@ func newTestConfig() TestConfig {
4646
"io.rancher.os.createonly": "true",
4747
"io.rancher.os.scope": "system",
4848
},
49-
Volumes: []string{
50-
"/dev:/host/dev",
51-
"/var/lib/rancher/conf:/var/lib/rancher/conf",
52-
"/etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt.rancher",
53-
"/lib/modules:/lib/modules",
54-
"/lib/firmware:/lib/firmware",
55-
"/var/run:/var/run",
56-
"/var/log:/var/log",
49+
Volumes: &yamlTypes.Volumes{
50+
Volumes: []*yamlTypes.Volume{
51+
{
52+
Source: "/dev",
53+
Destination: "/host/dev",
54+
},
55+
{
56+
Source: "/var/lib/rancher/conf",
57+
Destination: "/var/lib/rancher/conf",
58+
},
59+
{
60+
Source: "/etc/ssl/certs/ca-certificates.crt",
61+
Destination: "/etc/ssl/certs/ca-certificates.crt.rancher",
62+
},
63+
{
64+
Source: "/lib/modules",
65+
Destination: "lib/modules",
66+
},
67+
{
68+
Source: "/lib/firmware",
69+
Destination: "/lib/firmware",
70+
},
71+
{
72+
Source: "/var/run",
73+
Destination: "/var/run",
74+
},
75+
{
76+
Source: "/var/log",
77+
Destination: "/var/log",
78+
},
79+
},
5780
},
5881
Logging: Log{
5982
Driver: "json-file",

config/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ type ServiceConfig struct {
116116
ShmSize yaml.StringorInt `yaml:"shm_size,omitempty"`
117117
StopSignal string `yaml:"stop_signal,omitempty"`
118118
VolumeDriver string `yaml:"volume_driver,omitempty"`
119-
Volumes []string `yaml:"volumes,omitempty"`
119+
Volumes *yaml.Volumes `yaml:"volumes,omitempty"`
120120
VolumesFrom []string `yaml:"volumes_from,omitempty"`
121121
Uts string `yaml:"uts,omitempty"`
122122
Restart string `yaml:"restart,omitempty"`

docker/convert.go

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
composeclient "github.com/docker/libcompose/docker/client"
1717
"github.com/docker/libcompose/project"
1818
"github.com/docker/libcompose/utils"
19+
// "github.com/docker/libcompose/yaml"
1920
)
2021

2122
// ConfigWrapper wraps Config, HostConfig and NetworkingConfig for a container.
@@ -36,6 +37,16 @@ func Filter(vs []string, f func(string) bool) []string {
3637
return r
3738
}
3839

40+
func toMap(vs []string) map[string]struct{} {
41+
m := map[string]struct{}{}
42+
for _, v := range vs {
43+
if v != "" {
44+
m[v] = struct{}{}
45+
}
46+
}
47+
return m
48+
}
49+
3950
func isBind(s string) bool {
4051
return strings.ContainsRune(s, ':')
4152
}
@@ -58,21 +69,18 @@ func ConvertToAPI(serviceConfig *config.ServiceConfig, ctx project.Context, clie
5869
return &result, nil
5970
}
6071

61-
func isNamedVolume(volume string) bool {
62-
return !strings.HasPrefix(volume, ".") && !strings.HasPrefix(volume, "/") && !strings.HasPrefix(volume, "~")
63-
}
64-
65-
func volumes(c *config.ServiceConfig, ctx project.Context) map[string]struct{} {
66-
volumes := make(map[string]struct{}, len(c.Volumes))
67-
for k, v := range c.Volumes {
68-
if len(ctx.ComposeFiles) > 0 && !isNamedVolume(v) {
69-
v = ctx.ResourceLookup.ResolvePath(v, ctx.ComposeFiles[0])
70-
}
71-
72-
c.Volumes[k] = v
73-
if isVolume(v) {
74-
volumes[v] = struct{}{}
72+
func volumes(c *config.ServiceConfig, ctx project.Context) []string {
73+
if c.Volumes == nil {
74+
return []string{}
75+
}
76+
volumes := make([]string, len(c.Volumes.Volumes))
77+
for _, v := range c.Volumes.Volumes {
78+
vol := v
79+
if len(ctx.ComposeFiles) > 0 && !project.IsNamedVolume(v.Source) {
80+
sourceVol := ctx.ResourceLookup.ResolvePath(v.String(), ctx.ComposeFiles[0])
81+
vol.Source = strings.SplitN(sourceVol, ":", 2)[0]
7582
}
83+
volumes = append(volumes, vol.String())
7684
}
7785
return volumes
7886
}
@@ -141,6 +149,8 @@ func Convert(c *config.ServiceConfig, ctx project.Context, clientFactory compose
141149
}
142150
}
143151

152+
vols := volumes(c, ctx)
153+
144154
config := &container.Config{
145155
Entrypoint: strslice.StrSlice(utils.CopySlice(c.Entrypoint)),
146156
Hostname: c.Hostname,
@@ -154,7 +164,7 @@ func Convert(c *config.ServiceConfig, ctx project.Context, clientFactory compose
154164
Tty: c.Tty,
155165
OpenStdin: c.StdinOpen,
156166
WorkingDir: c.WorkingDir,
157-
Volumes: volumes(c, ctx),
167+
Volumes: toMap(Filter(vols, isVolume)),
158168
MacAddress: c.MacAddress,
159169
}
160170

@@ -228,7 +238,7 @@ func Convert(c *config.ServiceConfig, ctx project.Context, clientFactory compose
228238
CapDrop: strslice.StrSlice(utils.CopySlice(c.CapDrop)),
229239
ExtraHosts: utils.CopySlice(c.ExtraHosts),
230240
Privileged: c.Privileged,
231-
Binds: Filter(c.Volumes, isBind),
241+
Binds: Filter(vols, isBind),
232242
DNS: utils.CopySlice(c.DNS),
233243
DNSSearch: utils.CopySlice(c.DNSSearch),
234244
LogConfig: container.LogConfig{

docker/convert_test.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,29 @@ func TestParseBindsAndVolumes(t *testing.T) {
2626
abs, err := filepath.Abs(".")
2727
assert.Nil(t, err)
2828
cfg, hostCfg, err := Convert(&config.ServiceConfig{
29-
Volumes: []string{"/foo", "/home:/home", "/bar/baz", ".:/home", "/usr/lib:/usr/lib:ro"},
29+
Volumes: &yaml.Volumes{
30+
Volumes: []*yaml.Volume{
31+
{
32+
Destination: "/foo",
33+
},
34+
{
35+
Source: "/home",
36+
Destination: "/home",
37+
},
38+
{
39+
Destination: "/bar/baz",
40+
},
41+
{
42+
Source: ".",
43+
Destination: "/home",
44+
},
45+
{
46+
Source: "/usr/lib",
47+
Destination: "/usr/lib",
48+
AccessMode: "ro",
49+
},
50+
},
51+
},
3052
}, ctx.Context, nil)
3153
assert.Nil(t, err)
3254
assert.Equal(t, map[string]struct{}{"/foo": {}, "/bar/baz": {}}, cfg.Volumes)

docker/project.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/docker/libcompose/config"
1313
"github.com/docker/libcompose/docker/client"
1414
"github.com/docker/libcompose/docker/network"
15+
"github.com/docker/libcompose/docker/volume"
1516
"github.com/docker/libcompose/labels"
1617
"github.com/docker/libcompose/lookup"
1718
"github.com/docker/libcompose/project"
@@ -66,6 +67,13 @@ func NewProject(context *Context, parseOptions *config.ParseOptions) (project.AP
6667
context.NetworksFactory = networksFactory
6768
}
6869

70+
if context.VolumesFactory == nil {
71+
volumesFactory := &volume.DockerFactory{
72+
ClientFactory: context.ClientFactory,
73+
}
74+
context.VolumesFactory = volumesFactory
75+
}
76+
6977
// FIXME(vdemeester) Remove the context duplication ?
7078
runtime := &Project{
7179
clientFactory: context.ClientFactory,

docker/volume/volume.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package volume
2+
3+
import (
4+
"fmt"
5+
6+
"golang.org/x/net/context"
7+
8+
"github.com/docker/engine-api/client"
9+
"github.com/docker/engine-api/types"
10+
"github.com/docker/libcompose/config"
11+
12+
composeclient "github.com/docker/libcompose/docker/client"
13+
"github.com/docker/libcompose/project"
14+
)
15+
16+
// Volume holds attributes and method for a volume definition in compose
17+
type Volume struct {
18+
client client.VolumeAPIClient
19+
projectName string
20+
name string
21+
driver string
22+
driverOptions map[string]string
23+
external bool
24+
// TODO (shouze) missing labels
25+
}
26+
27+
func (v *Volume) fullName() string {
28+
name := v.projectName + "_" + v.name
29+
if v.external {
30+
name = v.name
31+
}
32+
return name
33+
}
34+
35+
// Inspect inspect the current volume
36+
func (v *Volume) Inspect(ctx context.Context) (types.Volume, error) {
37+
return v.client.VolumeInspect(ctx, v.fullName())
38+
}
39+
40+
// Remove removes the current volume (from docker engine)
41+
func (v *Volume) Remove(ctx context.Context) error {
42+
if v.external {
43+
fmt.Printf("Volume %s is external, skipping", v.fullName())
44+
return nil
45+
}
46+
fmt.Printf("Removing volume %q\n", v.fullName())
47+
return v.client.VolumeRemove(ctx, v.fullName())
48+
}
49+
50+
// EnsureItExists make sure the volume exists and return an error if it does not exists
51+
// and cannot be created.
52+
func (v *Volume) EnsureItExists(ctx context.Context) error {
53+
volumeResource, err := v.Inspect(ctx)
54+
if v.external {
55+
if client.IsErrVolumeNotFound(err) {
56+
// FIXME(shouze) introduce some libcompose error type
57+
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)
58+
}
59+
return err
60+
}
61+
if err != nil && client.IsErrVolumeNotFound(err) {
62+
return v.create(ctx)
63+
}
64+
if volumeResource.Driver != v.driver {
65+
return fmt.Errorf("Volume %q needs to be recreated - driver has changed", v.name)
66+
}
67+
return err
68+
}
69+
70+
func (v *Volume) create(ctx context.Context) error {
71+
fmt.Printf("Creating volume %q with driver %q\n", v.fullName(), v.driver)
72+
_, err := v.client.VolumeCreate(ctx, types.VolumeCreateRequest{
73+
Name: v.fullName(),
74+
Driver: v.driver,
75+
DriverOpts: v.driverOptions,
76+
// TODO (shouze) missing labels
77+
})
78+
79+
return err
80+
}
81+
82+
// NewVolume creates a new volume from the specified name and config.
83+
func NewVolume(projectName, name string, config *config.VolumeConfig, client client.VolumeAPIClient) *Volume {
84+
return &Volume{
85+
client: client,
86+
projectName: projectName,
87+
name: name,
88+
driver: config.Driver,
89+
driverOptions: config.DriverOpts,
90+
external: config.External.External,
91+
}
92+
}
93+
94+
// Volumes holds a list of volume
95+
type Volumes struct {
96+
volumes []*Volume
97+
volumeEnabled bool
98+
}
99+
100+
// Initialize make sure volume exists if volume is enabled
101+
func (v *Volumes) Initialize(ctx context.Context) error {
102+
if !v.volumeEnabled {
103+
return nil
104+
}
105+
for _, volume := range v.volumes {
106+
err := volume.EnsureItExists(ctx)
107+
if err != nil {
108+
return err
109+
}
110+
}
111+
return nil
112+
}
113+
114+
// Remove removes volumes (clean-up)
115+
func (v *Volumes) Remove(ctx context.Context) error {
116+
if !v.volumeEnabled {
117+
return nil
118+
}
119+
for _, volume := range v.volumes {
120+
err := volume.Remove(ctx)
121+
if err != nil {
122+
return err
123+
}
124+
}
125+
return nil
126+
}
127+
128+
// VolumesFromServices creates a new Volumes struct based on volumes configurations and
129+
// services configuration. If a volume is defined but not used by any service, it will return
130+
// an error along the Volumes.
131+
func VolumesFromServices(cli client.VolumeAPIClient, projectName string, volumeConfigs map[string]*config.VolumeConfig, services *config.ServiceConfigs, volumeEnabled bool) (*Volumes, error) {
132+
var err error
133+
volumes := make([]*Volume, 0, len(volumeConfigs))
134+
for name, config := range volumeConfigs {
135+
volume := NewVolume(projectName, name, config, cli)
136+
volumes = append(volumes, volume)
137+
}
138+
return &Volumes{
139+
volumes: volumes,
140+
volumeEnabled: volumeEnabled,
141+
}, err
142+
}
143+
144+
// DockerFactory implements project.VolumesFactory
145+
type DockerFactory struct {
146+
ClientFactory composeclient.Factory
147+
}
148+
149+
// Create implements project.VolumesFactory Create method.
150+
// It creates a Volumes (that implements project.Volumes) from specified configurations.
151+
func (f *DockerFactory) Create(projectName string, volumeConfigs map[string]*config.VolumeConfig, serviceConfigs *config.ServiceConfigs, volumeEnabled bool) (project.Volumes, error) {
152+
cli := f.ClientFactory.Create(nil)
153+
return VolumesFromServices(cli, projectName, volumeConfigs, serviceConfigs, volumeEnabled)
154+
}

0 commit comments

Comments
 (0)