Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for "Application failed to start after update" when an external network is on a watched service #11092

Merged
merged 6 commits into from
Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion pkg/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -1124,13 +1124,27 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne
networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("name", n.Name)),
})

if err != nil {
return err
}

if len(networks) == 0 {
// in this instance, n.Name is really an ID
sn, err := s.apiClient().NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
if err != nil {
return err
}
networks = append(networks, sn)

}

// NetworkList API doesn't return the exact name match, so we can retrieve more than one network with a request
networks = utils.Filter(networks, func(net moby.NetworkResource) bool {
return net.Name == n.Name
// later in this function, the name is changed the to ID.
// this function is called during the rebuild stage of `compose watch`.
// we still require just one network back, but we need to run the search on the ID
return net.Name == n.Name || net.ID == n.Name
})

switch len(networks) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/compose/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
},
})
if err != nil {
fmt.Fprintf(s.stderr(), "Application failed to start after update\n")
fmt.Fprintf(s.stderr(), "Application failed to start after update. Error: %v\n", err)
}
return nil
}
Expand Down
19 changes: 19 additions & 0 deletions pkg/e2e/fixtures/watch/with-external-network.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

services:
ext-alpine:
build:
dockerfile_inline: |-
FROM alpine
init: true
command: sleep infinity
develop:
watch:
- action: rebuild
path: .env
networks:
- external_network_test

networks:
external_network_test:
name: e2e-watch-external_network_test
external: true
93 changes: 93 additions & 0 deletions pkg/e2e/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,99 @@ func TestWatch(t *testing.T) {
})
}

func TestRebuildOnDotEnvWithExternalNetwork(t *testing.T) {
const projectName = "test_rebuild_on_dotenv_with_external_network"
const svcName = "ext-alpine"
containerName := strings.Join([]string{projectName, svcName, "1"}, "-")
const networkName = "e2e-watch-external_network_test"
const dotEnvFilepath = "./fixtures/watch/.env"

c := NewCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME="+projectName,
"COMPOSE_FILE=./fixtures/watch/with-external-network.yaml",
))

cleanup := func() {
c.RunDockerComposeCmdNoCheck(t, "down", "--remove-orphans", "--volumes", "--rmi=local")
c.RunDockerOrExitError(t, "network", "rm", networkName)
os.Remove(dotEnvFilepath) //nolint:errcheck
}
cleanup()

t.Log("create network that is referenced by the container we're testing")
c.RunDockerCmd(t, "network", "create", networkName)
res := c.RunDockerCmd(t, "network", "ls")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())

t.Log("create a dotenv file that will be used to trigger the rebuild")
err := os.WriteFile(dotEnvFilepath, []byte("HELLO=WORLD"), 0o666)
assert.NilError(t, err)
_, err = os.ReadFile(dotEnvFilepath)
assert.NilError(t, err)

// TODO: refactor this duplicated code into frameworks? Maybe?
t.Log("starting docker compose watch")
cmd := c.NewDockerComposeCmd(t, "--verbose", "watch", svcName)
// stream output since watch runs in the background
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
r := icmd.StartCmd(cmd)
require.NoError(t, r.Error)
var testComplete atomic.Bool
go func() {
// if the process exits abnormally before the test is done, fail the test
if err := r.Cmd.Wait(); err != nil && !t.Failed() && !testComplete.Load() {
assert.Check(t, cmp.Nil(err))
}
}()

t.Log("wait for watch to start watching")
c.WaitForCondition(t, func() (bool, string) {
out := r.String()
errors := r.String()
return strings.Contains(out,
"watching"), fmt.Sprintf("'watching' not found in : \n%s\nStderr: \n%s\n", out,
errors)
}, 30*time.Second, 1*time.Second)

n := c.RunDockerCmd(t, "network", "inspect", networkName, "-f", "{{ .Id }}")
pn := c.RunDockerCmd(t, "inspect", containerName, "-f", "{{ .HostConfig.NetworkMode }}")
assert.Equal(t, pn.Stdout(), n.Stdout())

t.Log("create a dotenv file that will be used to trigger the rebuild")
err = os.WriteFile(dotEnvFilepath, []byte("HELLO=WORLD\nTEST=REBUILD"), 0o666)
assert.NilError(t, err)
_, err = os.ReadFile(dotEnvFilepath)
assert.NilError(t, err)

// NOTE: are there any other ways to check if the container has been rebuilt?
t.Log("check if the container has been rebuild")
c.WaitForCondition(t, func() (bool, string) {
out := r.String()
if strings.Count(out, "batch complete: service["+svcName+"]") != 1 {
return false, fmt.Sprintf("container %s was not rebuilt", containerName)
}
return true, fmt.Sprintf("container %s was rebuilt", containerName)
}, 30*time.Second, 1*time.Second)

n2 := c.RunDockerCmd(t, "network", "inspect", networkName, "-f", "{{ .Id }}")
pn2 := c.RunDockerCmd(t, "inspect", containerName, "-f", "{{ .HostConfig.NetworkMode }}")
assert.Equal(t, pn2.Stdout(), n2.Stdout())

assert.Check(t, !strings.Contains(r.Combined(), "Application failed to start after update"))

t.Cleanup(cleanup)
t.Cleanup(func() {
// IMPORTANT: watch doesn't exit on its own, don't leak processes!
if r.Cmd.Process != nil {
t.Logf("Killing watch process: pid[%d]", r.Cmd.Process.Pid)
_ = r.Cmd.Process.Kill()
}
})
testComplete.Store(true)

}

// NOTE: these tests all share a single Compose file but are safe to run concurrently
func doTest(t *testing.T, svcName string, tarSync bool) {
tmpdir := t.TempDir()
Expand Down