Merge pull request #1143 from aiordache/local_volume_override

This commit is contained in:
Nicolas De loof 2021-01-18 21:20:34 +01:00 committed by GitHub
commit c881e22ab8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 205 additions and 81 deletions

View File

@ -93,7 +93,6 @@ func (s *composeService) ensureService(ctx context.Context, observedState Contai
} }
for _, container := range actual { for _, container := range actual {
container := container
name := getCanonicalContainerName(container) name := getCanonicalContainerName(container)
diverged := container.Labels[configHashLabel] != expected diverged := container.Labels[configHashLabel] != expected
@ -251,7 +250,7 @@ func (s *composeService) restartContainer(ctx context.Context, container moby.Co
} }
func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, container *moby.Container, autoRemove bool) error { func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, container *moby.Container, autoRemove bool) error {
containerConfig, hostConfig, networkingConfig, err := getCreateOptions(project, service, number, container, autoRemove) containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, container, autoRemove)
if err != nil { if err != nil {
return err return err
} }

View File

@ -50,6 +50,11 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt
prepareNetworks(project) prepareNetworks(project)
err = prepareVolumes(project)
if err != nil {
return err
}
if err := s.ensureNetworks(ctx, project.Networks); err != nil { if err := s.ensureNetworks(ctx, project.Networks); err != nil {
return err return err
} }
@ -91,6 +96,29 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt
}) })
} }
func prepareVolumes(p *types.Project) error {
for i := range p.Services {
volumesFrom, dependServices, err := getVolumesFrom(p, p.Services[i].VolumesFrom)
if err != nil {
return err
}
p.Services[i].VolumesFrom = volumesFrom
if len(dependServices) > 0 {
if p.Services[i].DependsOn == nil {
p.Services[i].DependsOn = make(types.DependsOnConfig, len(dependServices))
}
for _, service := range p.Services {
if contains(dependServices, service.Name) {
p.Services[i].DependsOn[service.Name] = types.ServiceDependency{
Condition: types.ServiceConditionStarted,
}
}
}
}
}
return nil
}
func prepareNetworks(project *types.Project) { func prepareNetworks(project *types.Project) {
for k, network := range project.Networks { for k, network := range project.Networks {
network.Labels = network.Labels.Add(networkLabel, k) network.Labels = network.Labels.Add(networkLabel, k)
@ -131,21 +159,23 @@ func getImageName(service types.ServiceConfig, projectName string) string {
return imageName return imageName
} }
func getCreateOptions(p *types.Project, s types.ServiceConfig, number int, inherit *moby.Container, autoRemove bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project, service types.ServiceConfig, number int, inherit *moby.Container,
hash, err := jsonHash(s) autoRemove bool) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) {
hash, err := jsonHash(service)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
labels := map[string]string{} labels := map[string]string{}
for k, v := range s.Labels { for k, v := range service.Labels {
labels[k] = v labels[k] = v
} }
labels[projectLabel] = p.Name labels[projectLabel] = p.Name
labels[serviceLabel] = s.Name labels[serviceLabel] = service.Name
labels[versionLabel] = ComposeVersion labels[versionLabel] = ComposeVersion
if _, ok := s.Labels[oneoffLabel]; !ok { if _, ok := service.Labels[oneoffLabel]; !ok {
labels[oneoffLabel] = "False" labels[oneoffLabel] = "False"
} }
labels[configHashLabel] = hash labels[configHashLabel] = hash
@ -157,24 +187,29 @@ func getCreateOptions(p *types.Project, s types.ServiceConfig, number int, inher
runCmd strslice.StrSlice runCmd strslice.StrSlice
entrypoint strslice.StrSlice entrypoint strslice.StrSlice
) )
if len(s.Command) > 0 { if len(service.Command) > 0 {
runCmd = strslice.StrSlice(s.Command) runCmd = strslice.StrSlice(service.Command)
} }
if len(s.Entrypoint) > 0 { if len(service.Entrypoint) > 0 {
entrypoint = strslice.StrSlice(s.Entrypoint) entrypoint = strslice.StrSlice(service.Entrypoint)
} }
var ( var (
tty = s.Tty tty = service.Tty
stdinOpen = s.StdinOpen stdinOpen = service.StdinOpen
attachStdin = false attachStdin = false
) )
volumeMounts, binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
if err != nil {
return nil, nil, nil, err
}
containerConfig := container.Config{ containerConfig := container.Config{
Hostname: s.Hostname, Hostname: service.Hostname,
Domainname: s.DomainName, Domainname: service.DomainName,
User: s.User, User: service.User,
ExposedPorts: buildContainerPorts(s), ExposedPorts: buildContainerPorts(service),
Tty: tty, Tty: tty,
OpenStdin: stdinOpen, OpenStdin: stdinOpen,
StdinOnce: true, StdinOnce: true,
@ -182,42 +217,42 @@ func getCreateOptions(p *types.Project, s types.ServiceConfig, number int, inher
AttachStderr: true, AttachStderr: true,
AttachStdout: true, AttachStdout: true,
Cmd: runCmd, Cmd: runCmd,
Image: getImageName(s, p.Name), Image: getImageName(service, p.Name),
WorkingDir: s.WorkingDir, WorkingDir: service.WorkingDir,
Entrypoint: entrypoint, Entrypoint: entrypoint,
NetworkDisabled: s.NetworkMode == "disabled", NetworkDisabled: service.NetworkMode == "disabled",
MacAddress: s.MacAddress, MacAddress: service.MacAddress,
Labels: labels, Labels: labels,
StopSignal: s.StopSignal, StopSignal: service.StopSignal,
Env: convert.ToMobyEnv(s.Environment), Env: convert.ToMobyEnv(service.Environment),
Healthcheck: convert.ToMobyHealthCheck(s.HealthCheck), Healthcheck: convert.ToMobyHealthCheck(service.HealthCheck),
// Volumes: // FIXME unclear to me the overlap with HostConfig.Mounts Volumes: volumeMounts,
StopTimeout: convert.ToSeconds(s.StopGracePeriod),
StopTimeout: convert.ToSeconds(service.StopGracePeriod),
} }
mountOptions, err := buildContainerMountOptions(*p, s, inherit) portBindings := buildContainerPortBindingOptions(service)
if err != nil {
return nil, nil, nil, err
}
bindings := buildContainerBindingOptions(s)
resources := getDeployResources(s) resources := getDeployResources(service)
networkMode := getNetworkMode(p, s) networkMode := getNetworkMode(p, service)
hostConfig := container.HostConfig{ hostConfig := container.HostConfig{
AutoRemove: autoRemove, AutoRemove: autoRemove,
Mounts: mountOptions, Binds: binds,
CapAdd: strslice.StrSlice(s.CapAdd), Mounts: mounts,
CapDrop: strslice.StrSlice(s.CapDrop), CapAdd: strslice.StrSlice(service.CapAdd),
CapDrop: strslice.StrSlice(service.CapDrop),
NetworkMode: networkMode, NetworkMode: networkMode,
Init: s.Init, Init: service.Init,
ReadonlyRootfs: s.ReadOnly, ReadonlyRootfs: service.ReadOnly,
// ShmSize: , TODO // ShmSize: , TODO
Sysctls: s.Sysctls, Sysctls: service.Sysctls,
PortBindings: bindings, PortBindings: portBindings,
Resources: resources, Resources: resources,
VolumeDriver: service.VolumeDriver,
VolumesFrom: service.VolumesFrom,
} }
networkConfig := buildDefaultNetworkConfig(s, networkMode, getContainerName(p.Name, s, number)) networkConfig := buildDefaultNetworkConfig(service, networkMode, getContainerName(p.Name, service, number))
return &containerConfig, &hostConfig, networkConfig, nil return &containerConfig, &hostConfig, networkConfig, nil
} }
@ -253,7 +288,7 @@ func buildContainerPorts(s types.ServiceConfig) nat.PortSet {
return ports return ports
} }
func buildContainerBindingOptions(s types.ServiceConfig) nat.PortMap { func buildContainerPortBindingOptions(s types.ServiceConfig) nat.PortMap {
bindings := nat.PortMap{} bindings := nat.PortMap{}
for _, port := range s.Ports { for _, port := range s.Ports {
p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol)) p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
@ -268,9 +303,71 @@ func buildContainerBindingOptions(s types.ServiceConfig) nat.PortMap {
return bindings return bindings
} }
func buildContainerMountOptions(p types.Project, s types.ServiceConfig, inherit *moby.Container) ([]mount.Mount, error) { func getVolumesFrom(project *types.Project, volumesFrom []string) ([]string, []string, error) {
mounts := []mount.Mount{} var volumes = []string{}
var inherited []string var services = []string{}
// parse volumes_from
if len(volumesFrom) == 0 {
return volumes, services, nil
}
for _, vol := range volumesFrom {
spec := strings.Split(vol, ":")
if spec[0] == "container" {
volumes = append(volumes, strings.Join(spec[1:], ":"))
continue
}
serviceName := spec[0]
services = append(services, serviceName)
service, err := project.GetService(serviceName)
if err != nil {
return nil, nil, err
}
firstContainer := getContainerName(project.Name, service, 1)
v := fmt.Sprintf("%s:%s", firstContainer, strings.Join(spec[1:], ":"))
volumes = append(volumes, v)
}
return volumes, services, nil
}
func (s *composeService) buildContainerVolumes(ctx context.Context, p types.Project, service types.ServiceConfig,
inherit *moby.Container) (map[string]struct{}, []string, []mount.Mount, error) {
var mounts = []mount.Mount{}
image := getImageName(service, p.Name)
imgInspect, _, err := s.apiClient.ImageInspectWithRaw(ctx, image)
if err != nil {
return nil, nil, nil, err
}
mountOptions, err := buildContainerMountOptions(p, service, imgInspect, inherit)
if err != nil {
return nil, nil, nil, err
}
// filter binds and volumes mount targets
volumeMounts := map[string]struct{}{}
binds := []string{}
for _, m := range mountOptions {
if m.Type == mount.TypeVolume {
volumeMounts[m.Target] = struct{}{}
if m.Source != "" {
binds = append(binds, fmt.Sprintf("%s:%s:rw", m.Source, m.Target))
}
}
}
for _, m := range mountOptions {
if m.Type == mount.TypeBind || m.Type == mount.TypeTmpfs {
mounts = append(mounts, m)
}
}
return volumeMounts, binds, mounts, nil
}
func buildContainerMountOptions(p types.Project, s types.ServiceConfig, img moby.ImageInspect, inherit *moby.Container) ([]mount.Mount, error) {
var mounts = map[string]mount.Mount{}
if inherit != nil { if inherit != nil {
for _, m := range inherit.Mounts { for _, m := range inherit.Mounts {
if m.Type == "tmpfs" { if m.Type == "tmpfs" {
@ -280,27 +377,56 @@ func buildContainerMountOptions(p types.Project, s types.ServiceConfig, inherit
if m.Type == "volume" { if m.Type == "volume" {
src = m.Name src = m.Name
} }
mounts = append(mounts, mount.Mount{ mounts[m.Destination] = mount.Mount{
Type: m.Type, Type: m.Type,
Source: src, Source: src,
Target: m.Destination, Target: m.Destination,
ReadOnly: !m.RW, ReadOnly: !m.RW,
}) }
inherited = append(inherited, m.Destination)
} }
} }
if img.ContainerConfig != nil {
for k := range img.ContainerConfig.Volumes {
mount, err := buildMount(p, types.ServiceVolumeConfig{
Type: types.VolumeTypeVolume,
Target: k,
})
if err != nil {
return nil, err
}
mounts[k] = mount
for _, v := range s.Volumes {
if contains(inherited, v.Target) {
continue
} }
}
for _, v := range s.Volumes {
mount, err := buildMount(p, v) mount, err := buildMount(p, v)
if err != nil { if err != nil {
return nil, err return nil, err
} }
mounts = append(mounts, mount) mounts[mount.Target] = mount
} }
secrets, err := buildContainerSecretMounts(p, s)
if err != nil {
return nil, err
}
for _, s := range secrets {
if _, found := mounts[s.Target]; found {
continue
}
mounts[s.Target] = s
}
values := make([]mount.Mount, 0, len(mounts))
for _, v := range mounts {
values = append(values, v)
}
return values, nil
}
func buildContainerSecretMounts(p types.Project, s types.ServiceConfig) ([]mount.Mount, error) {
var mounts = map[string]mount.Mount{}
secretsDir := "/run/secrets" secretsDir := "/run/secrets"
for _, secret := range s.Secrets { for _, secret := range s.Secrets {
target := secret.Target target := secret.Target
@ -315,27 +441,22 @@ func buildContainerMountOptions(p types.Project, s types.ServiceConfig, inherit
return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name) return nil, fmt.Errorf("unsupported external secret %s", definedSecret.Name)
} }
if contains(inherited, target) {
// remove inherited mount
pos := indexOf(inherited, target)
if pos >= 0 {
mounts = append(mounts[:pos], mounts[pos+1])
inherited = append(inherited[:pos], inherited[pos+1])
}
}
mount, err := buildMount(p, types.ServiceVolumeConfig{ mount, err := buildMount(p, types.ServiceVolumeConfig{
Type: types.VolumeTypeBind, Type: types.VolumeTypeBind,
Source: definedSecret.File, Source: definedSecret.File,
Target: target, Target: target,
ReadOnly: true,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
mounts = append(mounts, mount) mounts[target] = mount
} }
values := make([]mount.Mount, 0, len(mounts))
return mounts, nil for _, v := range mounts {
values = append(values, v)
}
return values, nil
} }
func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) { func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.Mount, error) {
@ -349,10 +470,14 @@ func buildMount(project types.Project, volume types.ServiceVolumeConfig) (mount.
} }
} }
if volume.Type == types.VolumeTypeVolume { if volume.Type == types.VolumeTypeVolume {
pVolume, ok := project.Volumes[volume.Source] if volume.Source != "" {
if ok {
source = pVolume.Name pVolume, ok := project.Volumes[volume.Source]
if ok {
source = pVolume.Name
}
} }
} }
return mount.Mount{ return mount.Mount{

View File

@ -38,12 +38,3 @@ func contains(slice []string, item string) bool {
} }
return false return false
} }
func indexOf(slice []string, item string) int {
for i, v := range slice {
if v == item {
return i
}
}
return -1
}

View File

@ -261,9 +261,18 @@ func TestLocalComposeVolume(t *testing.T) {
}) })
t.Run("check container volume specs", func(t *testing.T) { t.Run("check container volume specs", func(t *testing.T) {
res := c.RunDockerCmd("inspect", "compose-e2e-volume_nginx2_1", "--format", "{{ json .HostConfig.Mounts }}") res := c.RunDockerCmd("inspect", "compose-e2e-volume_nginx2_1", "--format", "{{ json .Mounts }}")
output := res.Stdout()
//nolint //nolint
res.Assert(t, icmd.Expected{Out: `[{"Type":"volume","Source":"compose-e2e-volume_staticVol","Target":"/usr/share/nginx/html","ReadOnly":true},{"Type":"volume","Target":"/usr/src/app/node_modules"},{"Type":"volume","Source":"myVolume","Target":"/usr/share/nginx/test"}]`}) assert.Assert(t, strings.Contains(output, `"Destination":"/usr/src/app/node_modules","Driver":"local","Mode":"","RW":true,"Propagation":""`))
})
t.Run("check container bind-mounts specs", func(t *testing.T) {
res := c.RunDockerCmd("inspect", "compose-e2e-volume_nginx_1", "--format", "{{ json .HostConfig.Mounts }}")
output := res.Stdout()
//nolint
assert.Assert(t, strings.Contains(output, `"Type":"bind"`))
assert.Assert(t, strings.Contains(output, `"Target":"/usr/share/nginx/html"`))
}) })
t.Run("cleanup volume project", func(t *testing.T) { t.Run("cleanup volume project", func(t *testing.T) {