don't rely on depends_on to resolve volume_from, better use observed state

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2023-09-14 09:11:27 +02:00 committed by Nicolas De loof
parent e19232e8a3
commit f6e31dbc6a
6 changed files with 80 additions and 115 deletions

View File

@ -178,6 +178,11 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
eg, _ := errgroup.WithContext(ctx) eg, _ := errgroup.WithContext(ctx)
err = c.resolveVolumeFrom(&service)
if err != nil {
return err
}
sort.Slice(containers, func(i, j int) bool { sort.Slice(containers, func(i, j int) bool {
return containers[i].Created < containers[j].Created return containers[i].Created < containers[j].Created
}) })
@ -258,6 +263,26 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
return err return err
} }
func (c *convergence) resolveVolumeFrom(service *types.ServiceConfig) error {
for i, vol := range service.VolumesFrom {
spec := strings.Split(vol, ":")
if len(spec) == 0 {
continue
}
if spec[0] == "container" {
service.VolumesFrom[i] = spec[1]
continue
}
name := spec[0]
dependencies := c.getObservedState(name)
if len(dependencies) == 0 {
return fmt.Errorf("cannot share volume with service %s: container missing", name)
}
service.VolumesFrom[i] = dependencies.sorted()[0].ID
}
return nil
}
func mustRecreate(expected types.ServiceConfig, actual moby.Container, policy string) (bool, error) { func mustRecreate(expected types.ServiceConfig, actual moby.Container, policy string) (bool, error) {
if policy == api.RecreateNever { if policy == api.RecreateNever {
return false, nil return false, nil

View File

@ -86,11 +86,6 @@ 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
} }
@ -123,31 +118,6 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
return newConvergence(options.Services, observedState, s).apply(ctx, project, options) return newConvergence(options.Services, observedState, s).apply(ctx, project, options)
} }
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 utils.StringContains(dependServices, service.Name) &&
p.Services[i].DependsOn[service.Name].Condition == "" {
p.Services[i].DependsOn[service.Name] = types.ServiceDependency{
Condition: types.ServiceConditionStarted,
Required: true,
}
}
}
}
}
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(api.NetworkLabel, k) network.Labels = network.Labels.Add(api.NetworkLabel, k)
@ -249,13 +219,6 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
if err != nil { if err != nil {
return createConfigs{}, err return createConfigs{}, err
} }
var volumesFrom []string
for _, v := range service.VolumesFrom {
if !strings.HasPrefix(v, "container:") {
return createConfigs{}, fmt.Errorf("invalid volume_from: %s", v)
}
volumesFrom = append(volumesFrom, v[len("container:"):])
}
// NETWORKING // NETWORKING
links, err := s.getLinks(ctx, p.Name, service, number) links, err := s.getLinks(ctx, p.Name, service, number)
@ -296,7 +259,7 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
PortBindings: portBindings, PortBindings: portBindings,
Resources: resources, Resources: resources,
VolumeDriver: service.VolumeDriver, VolumeDriver: service.VolumeDriver,
VolumesFrom: volumesFrom, VolumesFrom: service.VolumesFrom,
DNS: service.DNS, DNS: service.DNS,
DNSSearch: service.DNSSearch, DNSSearch: service.DNSSearch,
DNSOptions: service.DNSOpts, DNSOptions: service.DNSOpts,
@ -676,40 +639,6 @@ func buildContainerPortBindingOptions(s types.ServiceConfig) nat.PortMap {
return bindings return bindings
} }
func getVolumesFrom(project *types.Project, volumesFrom []string) ([]string, []string, error) {
var volumes = []string{}
var services = []string{}
// parse volumes_from
if len(volumesFrom) == 0 {
return volumes, services, nil
}
for _, vol := range volumesFrom {
spec := strings.Split(vol, ":")
if len(spec) == 0 {
continue
}
if spec[0] == "container" {
volumes = append(volumes, vol)
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("container:%s", firstContainer)
if len(spec) > 2 {
v = fmt.Sprintf("container:%s:%s", firstContainer, strings.Join(spec[1:], ":"))
}
volumes = append(volumes, v)
}
return volumes, services, nil
}
func getDependentServiceFromMode(mode string) string { func getDependentServiceFromMode(mode string) string {
if strings.HasPrefix( if strings.HasPrefix(
mode, mode,

View File

@ -98,46 +98,6 @@ func TestPrepareNetworkLabels(t *testing.T) {
})) }))
} }
func TestPrepareVolumes(t *testing.T) {
t.Run("adds dependency condition if service depends on volume from another service", func(t *testing.T) {
project := composetypes.Project{
Name: "myProject",
Services: []composetypes.ServiceConfig{
{
Name: "aService",
VolumesFrom: []string{"anotherService"},
},
{
Name: "anotherService",
},
},
}
err := prepareVolumes(&project)
assert.NilError(t, err)
assert.Equal(t, project.Services[0].DependsOn["anotherService"].Condition, composetypes.ServiceConditionStarted)
})
t.Run("doesn't overwrite existing dependency condition", func(t *testing.T) {
project := composetypes.Project{
Name: "myProject",
Services: []composetypes.ServiceConfig{
{
Name: "aService",
VolumesFrom: []string{"anotherService"},
DependsOn: map[string]composetypes.ServiceDependency{
"anotherService": {Condition: composetypes.ServiceConditionHealthy, Required: true},
},
},
{
Name: "anotherService",
},
},
}
err := prepareVolumes(&project)
assert.NilError(t, err)
assert.Equal(t, project.Services[0].DependsOn["anotherService"].Condition, composetypes.ServiceConditionHealthy)
})
}
func TestBuildContainerMountOptions(t *testing.T) { func TestBuildContainerMountOptions(t *testing.T) {
project := composetypes.Project{ project := composetypes.Project{
Name: "myProject", Name: "myProject",

View File

@ -58,9 +58,6 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
} }
func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) { func (s *composeService) prepareRun(ctx context.Context, project *types.Project, opts api.RunOptions) (string, error) {
if err := prepareVolumes(project); err != nil { // all dependencies already checked, but might miss service img
return "", err
}
service, err := project.GetService(opts.Service) service, err := project.GetService(opts.Service)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -0,0 +1,10 @@
services:
app:
image: nginx:alpine
volumes_from:
- db
db:
image: nginx:alpine
volumes:
- /var/data

44
pkg/e2e/noDeps_test.go Normal file
View File

@ -0,0 +1,44 @@
//go:build !windows
// +build !windows
/*
Copyright 2022 Docker Compose CLI authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package e2e
import (
"fmt"
"testing"
"gotest.tools/v3/icmd"
)
func TestNoDepsVolumeFrom(t *testing.T) {
c := NewParallelCLI(t)
const projectName = "e2e-no-deps-volume-from"
t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down")
})
c.RunDockerComposeCmd(t, "-f", "fixtures/no-deps/volume-from.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "-f", "fixtures/no-deps/volume-from.yaml", "--project-name", projectName, "up", "--no-deps", "-d", "app")
c.RunDockerCmd(t, "rm", "-f", fmt.Sprintf("%s-db-1", projectName))
res := c.RunDockerComposeCmdNoCheck(t, "-f", "fixtures/no-deps/volume-from.yaml", "--project-name", projectName, "up", "--no-deps", "-d", "app")
res.Assert(t, icmd.Expected{ExitCode: 1, Err: "cannot share volume with service db: container missing"})
}